From c4bb15e71cb003b3b09abd9aeb0f5c24c6ecdb32 Mon Sep 17 00:00:00 2001 From: guac420 <60993440+guac42@users.noreply.github.com> Date: Fri, 10 Mar 2023 20:54:24 -0800 Subject: [PATCH 01/32] Initial Transferring progress --- crates/valence/Cargo.toml | 2 +- crates/valence/examples/chat.rs | 346 +++++++++++++++++- .../examples/yggdrasil_session_pubkey.der | Bin 0 -> 550 bytes crates/valence/src/client/event.rs | 23 +- crates/valence/src/player_list.rs | 51 ++- .../src/packet/c2s/play/chat_message.rs | 2 +- .../packet/c2s/play/message_acknowledgment.rs | 2 +- .../src/packet/c2s/play/player_session.rs | 25 +- .../src/packet/s2c/play/player_list.rs | 12 +- 9 files changed, 420 insertions(+), 43 deletions(-) create mode 100644 crates/valence/examples/yggdrasil_session_pubkey.der diff --git a/crates/valence/Cargo.toml b/crates/valence/Cargo.toml index 459f08e63..f4c28ee1f 100644 --- a/crates/valence/Cargo.toml +++ b/crates/valence/Cargo.toml @@ -32,7 +32,7 @@ rsa-der = "0.3.0" rustc-hash = "1.1.0" serde = { version = "1.0.145", features = ["derive"] } serde_json = "1.0.85" -sha1 = "0.10.5" +sha1 = { version = "0.10.5", features = ["oid"] } sha2 = "0.10.6" thiserror = "1.0.35" tokio = { version = "1.25.0", features = ["full"] } diff --git a/crates/valence/examples/chat.rs b/crates/valence/examples/chat.rs index b5b6359e8..c6307a1a8 100644 --- a/crates/valence/examples/chat.rs +++ b/crates/valence/examples/chat.rs @@ -1,25 +1,130 @@ +use std::time::SystemTime; + use bevy_app::App; -use tracing::warn; +use rsa::pkcs8::DecodePublicKey; +use rsa::{PaddingScheme, PublicKey, RsaPublicKey}; +use sha1::{Digest, Sha1}; +use tracing::{debug, info, Level, warn}; use valence::client::despawn_disconnected_clients; -use valence::client::event::{default_event_handler, ChatMessage, CommandExecution}; +use valence::client::event::{ + default_event_handler, ChatMessage, ClientSettings, CommandExecution, MessageAcknowledgment, + PlayerSession, +}; use valence::prelude::*; +use valence::protocol::packet::c2s::play::client_settings::ChatMode; +use valence::protocol::translation_key::{ + CHAT_DISABLED_OPTIONS, MULTIPLAYER_DISCONNECT_CHAT_VALIDATION_FAILED, + MULTIPLAYER_DISCONNECT_EXPIRED_PUBLIC_KEY, MULTIPLAYER_DISCONNECT_INVALID_PUBLIC_KEY_SIGNATURE, + MULTIPLAYER_DISCONNECT_OUT_OF_ORDER_CHAT, +}; const SPAWN_Y: i32 = 64; +const MOJANG_KEY_DATA: &[u8] = include_bytes!("./yggdrasil_session_pubkey.der"); + +#[derive(Resource)] +struct MojangServicesState { + public_key: RsaPublicKey, +} + +#[derive(Debug, Default)] +struct AcknowledgementValidator { + messages: Vec>, + last_signature: Option>, +} + +#[derive(Debug)] +struct AcknowledgedMessage { + pub signature: Box<[u8; 256]>, + pub pending: bool, +} + +impl AcknowledgementValidator { + pub fn remove_until(&mut self, index: i32) -> bool { + if index >= 0 && index <= (self.messages.len() - 20) as i32 { + self.messages.drain(0..index as usize); + return true; + } + false + } + + pub fn validate( + &mut self, + acknowledgements: &[u8; 3], + message_index: i32, + ) -> Option> { + if !self.remove_until(message_index) { + // Invalid message index + return None; + } + + let acknowledged_count = { + let mut sum = 0u32; + for byte in acknowledgements { + sum += byte.count_ones(); + } + sum as usize + }; + + if acknowledged_count > 20 { + // Too many message acknowledgements, protocol error? + return None; + } + + let mut list = Vec::with_capacity(acknowledged_count); + for i in 0..20 { + let acknowledgement = acknowledgements[i >> 3] & (0b1 << (i % 8)) != 0; + let acknowledged_message = unsafe { self.messages.get_unchecked_mut(i) }; + if acknowledgement { + // Client has acknowledged the i-th message + if let Some(m) = acknowledged_message { + // The validator has the i-th message + m.pending = false; + list.push(*m.signature); + } else { + // Client has acknowledged a non-existing message + return None; + } + } + // Client has not acknowledged the i-th message + if matches!(acknowledged_message, Some(m) if !m.pending) { + // The validator has an i-th message that has already been validated + return None; + } + // If the validator has doesn't have an i-th message or it is pending + unsafe { + let m = self.messages.get_unchecked_mut(i); + *m = None; + } + } + Some(list) + } +} + +#[derive(Component)] +struct ChatState { + last_message_timestamp: u64, + chat_mode: ChatMode, + validator: AcknowledgementValidator, + public_key: Option, +} + pub fn main() { - tracing_subscriber::fmt().init(); + tracing_subscriber::fmt().with_max_level(Level::DEBUG).init(); App::new() - .add_plugin(ServerPlugin::new(()).with_connection_mode(ConnectionMode::Offline)) + .add_plugin(ServerPlugin::new(())) .add_startup_system(setup) .add_system(init_clients) .add_systems( ( default_event_handler, + handle_session_events, handle_message_events, + handle_message_acknowledgement, handle_command_events, ) - .in_schedule(EventLoopSchedule), + .in_schedule(EventLoopSchedule) ) .add_systems(PlayerList::default_systems()) .add_system(despawn_disconnected_clients) @@ -27,6 +132,11 @@ pub fn main() { } fn setup(mut commands: Commands, server: Res) { + let mojang_pub_key = RsaPublicKey::from_public_key_der(MOJANG_KEY_DATA) + .expect("Error creating Mojang public key"); + + commands.insert_resource(MojangServicesState { public_key: mojang_pub_key }); + let mut instance = server.new_instance(DimensionId::default()); for z in -5..5 { @@ -45,30 +155,221 @@ fn setup(mut commands: Commands, server: Res) { } fn init_clients( - mut clients: Query<&mut Client, Added>, + mut commands: Commands, + mut clients: Query<(Entity, &mut Client), Added>, instances: Query>, ) { - for mut client in &mut clients { + let instance = instances.single(); + + for (entity, mut client) in &mut clients { client.set_position([0.0, SPAWN_Y as f64 + 1.0, 0.0]); - client.set_instance(instances.single()); client.set_game_mode(GameMode::Adventure); client.send_message("Welcome to Valence! Talk about something.".italic()); + client.set_instance(instance); + + let mut state = ChatState { + last_message_timestamp: SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .expect("Unable to get Unix time") + .as_millis() as u64, + chat_mode: ChatMode::Enabled, + validator: Default::default(), + public_key: None, + }; + + commands.entity(entity).insert(state); + + info!("{} logged in!", client.username()); } } -fn handle_message_events(mut clients: Query<&mut Client>, mut messages: EventReader) { +fn handle_session_events( + services_state: Res, + player_list: ResMut, + mut clients: Query<&mut Client>, + mut states: Query<&mut ChatState>, + mut sessions: EventReader, +) { + let pl = player_list.into_inner(); + + for session in sessions.iter() { + let Ok(mut client) = clients.get_component_mut::(session.client) else { + warn!("Unable to find client for session"); + continue; + }; + + let Some(player_entry) = pl.get_mut(client.uuid()) else { + warn!("Unable to find '{}' in the player list", client.username()); + continue; + }; + + // Verify that the session key has not expired + if SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .expect("Unable to get Unix time") + .as_millis() + >= session.session_data.expires_at as u128 + { + warn!("Failed to validate profile key: expired public key"); + client.kick(Text::translate( + MULTIPLAYER_DISCONNECT_EXPIRED_PUBLIC_KEY, + [], + )); + continue; + } + + // Serialize the session data + let mut serialized = Vec::with_capacity(318); + serialized.extend_from_slice(client.uuid().into_bytes().as_slice()); + serialized.extend_from_slice(session.session_data.expires_at.to_be_bytes().as_ref()); + serialized.extend_from_slice(session.session_data.public_key_data.as_ref()); + + // Hash the session data using the SHA-1 algorithm + let mut hasher = Sha1::new(); + hasher.update(&serialized); + let hash = hasher.finalize(); + + // Verify the session key signature using the hashed session data and the Mojang + // public key + if services_state + .public_key + .verify( + PaddingScheme::new_pkcs1v15_sign::(), + &hash, + session.session_data.key_signature.as_ref(), + ) + .is_err() + { + warn!("Failed to validate profile key: invalid public key signature"); + client.kick(Text::translate( + MULTIPLAYER_DISCONNECT_INVALID_PUBLIC_KEY_SIGNATURE, + [], + )); + } + + let Ok(mut state) = states.get_component_mut::(session.client) else { + warn!("Unable to find chat state for client '{:?}'", client.username()); + continue; + }; + + if let Ok(public_key) = RsaPublicKey::from_public_key_der(session.session_data.public_key_data.as_ref()) { + state.public_key = Some(public_key); + } else { + // This shouldn't happen considering that it is highly unlikely that Mojang would + // provide the client with a malformed key. By this point the key signature has + // been verified + warn!("Received malformed profile key data from '{}'", client.username()); + client.kick(Text::translate( + MULTIPLAYER_DISCONNECT_EXPIRED_PUBLIC_KEY, + ["Malformed profile key data".color(Color::RED)], + )); + } + + player_entry.set_chat_data(Some(session.session_data.clone())); + } +} + +fn handle_message_acknowledgement( + mut clients: Query<&mut Client>, + mut states: Query<&mut ChatState>, + mut acknowledgements: EventReader, +) { + for acknowledgement in acknowledgements.iter() { + let Ok(mut client) = clients.get_component_mut::(acknowledgement.client) else { + warn!("Unable to find client for acknowledgement"); + continue; + }; + + let Ok(mut state) = states.get_component_mut::(acknowledgement.client) else { + warn!("Unable to find chat state for client '{:?}'", client.username()); + continue; + }; + + if !state.validator.remove_until(acknowledgement.message_index) { + warn!( + "Failed to validate message acknowledgement from '{:?}'", + client.username() + ); + client.kick(Text::translate( + MULTIPLAYER_DISCONNECT_CHAT_VALIDATION_FAILED, + [], + )); + continue; + } + + debug!("Acknowledgement from '{:?}'", client.username()); + } +} + +fn handle_message_events( + player_list: ResMut, + mut clients: Query<&mut Client>, + mut states: Query<&mut ChatState>, + mut messages: EventReader, +) { + let pl = player_list.into_inner(); + for message in messages.iter() { - let Ok(client) = clients.get_component::(message.client) else { - warn!("Unable to find client for message: {:?}", message); + let Ok(mut client) = clients.get_component_mut::(message.client) else { + warn!("Unable to find client for message '{:?}'", message); + continue; + }; + + let Ok(mut state) = states.get_component_mut::(message.client) else { + warn!("Unable to find chat state for client '{:?}'", client.username()); + continue; + }; + + if state.chat_mode == ChatMode::Hidden { + client.send_message(Text::translate(CHAT_DISABLED_OPTIONS, []).color(Color::RED)); + continue; + } + + let message_text = message.message.to_string(); + + if message.timestamp < state.last_message_timestamp { + warn!( + "{:?} sent out-of-order chat: '{:?}'", + client.username(), + message_text + ); + client.kick(Text::translate( + MULTIPLAYER_DISCONNECT_OUT_OF_ORDER_CHAT, + [], + )); + continue; + } + + state.last_message_timestamp = message.timestamp; + + let Some(player_entry) = pl.get_mut(client.uuid()) else { + warn!("Unable to find '{}' in the player list", client.username()); continue; }; - let message = message.message.to_string(); + match state.validator.validate(&message.acknowledgements, message.message_index) { + Some(last_seen) => { + // This process should probably be done on another thread similarly to chunk loading + // in the 'terrain.rs' example, as this is what the notchian server does + + } + None => { + warn!( + "Failed to validate acknowledgements from {:?}", + client.username() + ); + client.kick(MULTIPLAYER_DISCONNECT_CHAT_VALIDATION_FAILED); + } + }; + + info!("{}: {}", client.username(), message_text); + + // ############################################ let formatted = format!("<{}>: ", client.username()) .bold() .color(Color::YELLOW) - + message.into_text().not_bold().color(Color::WHITE); + + message_text.into_text().not_bold().color(Color::WHITE); // TODO: write message to instance buffer. for mut client in &mut clients { @@ -95,3 +396,22 @@ fn handle_command_events( client.send_message(formatted); } } + +fn handle_chat_settings_event( + mut states: Query<&mut ChatState>, + mut settings: EventReader, +) { + for ClientSettings { + client, chat_mode, .. + } in settings.iter() + { + let Ok(mut state) = states.get_component_mut::(*client) else { + warn!("Unable to find chat state for client"); + continue; + }; + + state.chat_mode = *chat_mode; + + debug!("Client settings: {:?}", chat_mode); + } +} diff --git a/crates/valence/examples/yggdrasil_session_pubkey.der b/crates/valence/examples/yggdrasil_session_pubkey.der new file mode 100644 index 0000000000000000000000000000000000000000..9c79a3aa4771da1f15af37a2af0898f878ad816f GIT binary patch literal 550 zcmV+>0@?jAf&wBi4F(A+hDe6@4FLfG1potr0uKN%f&vNxf&u{m%20R*skxUv>`)yD7%qARQ>bP36CtP}x@~Z9*VkPYUS6pWrZtT#q)8GCbf0*5+4Pyw;#Tq!Aq{bDO|Iw<43LnG*#4;?Z&LM71zXE@J`@_dMl`?qQ1 z@3qR;mm=XG_-{G#4vhRxmZiQslrTtB_&7&0ECnD9)gZ}j`e&Je+s, pub timestamp: u64, + pub salt: u64, + pub signature: Option>, + pub message_index: i32, + pub acknowledgements: [u8; 3], } #[derive(Clone, Debug)] @@ -330,10 +335,7 @@ pub struct PlayPong { #[derive(Clone, Debug)] pub struct PlayerSession { pub client: Entity, - pub session_id: Uuid, - pub expires_at: i64, - pub public_key_data: Box<[u8]>, - pub key_signature: Box<[u8]>, + pub session_data: PlayerSessionData, } #[derive(Clone, Debug)] @@ -756,7 +758,7 @@ fn handle_one_packet( C2sPlayPacket::MessageAcknowledgmentC2s(p) => { events.0.message_acknowledgment.send(MessageAcknowledgment { client: entity, - message_count: p.message_count.0, + message_index: p.message_index.0, }); } C2sPlayPacket::CommandExecutionC2s(p) => { @@ -771,6 +773,10 @@ fn handle_one_packet( client: entity, message: p.message.into(), timestamp: p.timestamp, + salt: p.salt, + signature: p.signature.copied().map(Box::new), + message_index: p.message_index.0, + acknowledgements: p.acknowledgement, }); } C2sPlayPacket::ClientStatusC2s(p) => match p { @@ -1151,10 +1157,7 @@ fn handle_one_packet( C2sPlayPacket::PlayerSessionC2s(p) => { events.3.player_session.send(PlayerSession { client: entity, - session_id: p.session_id, - expires_at: p.expires_at, - public_key_data: p.public_key_data.into(), - key_signature: p.key_signature.into(), + session_data: p.0.into_owned(), }); } C2sPlayPacket::RecipeCategoryOptionsC2s(p) => { diff --git a/crates/valence/src/player_list.rs b/crates/valence/src/player_list.rs index f5d2693b0..9e6d7ce0a 100644 --- a/crates/valence/src/player_list.rs +++ b/crates/valence/src/player_list.rs @@ -8,6 +8,7 @@ use bevy_ecs::prelude::*; use bevy_ecs::schedule::SystemConfigs; use tracing::warn; use uuid::Uuid; +use valence_protocol::packet::c2s::play::player_session::PlayerSessionData; use valence_protocol::packet::s2c::play::player_list::{ Actions, Entry as PlayerInfoEntry, PlayerListS2c, }; @@ -222,6 +223,7 @@ impl PlayerList { pub(crate) fn write_init_packets(&self, mut writer: impl WritePacket) { let actions = Actions::new() .with_add_player(true) + .with_initialize_chat(true) .with_update_game_mode(true) .with_update_listed(true) .with_update_latency(true) @@ -235,7 +237,7 @@ impl PlayerList { player_uuid: uuid, username: &entry.username, properties: entry.properties().into(), - chat_data: None, + chat_data: entry.chat_data.as_ref().map(|d| d.into()), listed: entry.listed, ping: entry.ping, game_mode: entry.game_mode, @@ -273,6 +275,7 @@ impl PlayerList { pub struct PlayerListEntry { username: String, // TODO: Username? properties: Vec, + chat_data: Option, game_mode: GameMode, old_game_mode: GameMode, ping: i32, @@ -281,6 +284,7 @@ pub struct PlayerListEntry { old_listed: bool, is_new: bool, modified_ping: bool, + modified_chat_data: bool, modified_display_name: bool, } @@ -289,6 +293,7 @@ impl Default for PlayerListEntry { Self { username: String::new(), properties: vec![], + chat_data: None, game_mode: GameMode::default(), old_game_mode: GameMode::default(), ping: -1, // Negative indicates absence. @@ -297,6 +302,7 @@ impl Default for PlayerListEntry { listed: true, is_new: true, modified_ping: false, + modified_chat_data: false, modified_display_name: false, } } @@ -342,6 +348,18 @@ impl PlayerListEntry { self } + /// Set the chat data for the player list entry. Returns `Self` to chain + /// other options. + /// + /// The chat data contains information about the player's chat session. + /// This includes the session id and the player's public key data and + /// expiration. This data is used for chat verification amongst players. + #[must_use] + pub fn with_chat_data(mut self, chat_data: Option) -> Self { + self.chat_data = chat_data; + self + } + /// Set the game mode for the player list entry. Returns `Self` to chain /// other options. #[must_use] @@ -393,6 +411,23 @@ impl PlayerListEntry { &self.properties } + pub fn chat_data(&self) -> Option<&PlayerSessionData> { + self.chat_data.as_ref() + } + + /// Set the chat data for the player list entry. + /// + /// The chat data contains information about the player's chat session. + /// This includes the session id and the player's public key data and + /// expiration. This data is used for chat verification amongst players. + pub fn set_chat_data(&mut self, chat_data: Option) { + if self.chat_data != chat_data { + self.modified_chat_data = true; + } + + self.chat_data = chat_data; + } + pub fn game_mode(&self) -> GameMode { self.game_mode } @@ -454,6 +489,7 @@ impl PlayerListEntry { self.old_game_mode = self.game_mode; self.old_listed = self.listed; self.modified_ping = false; + self.modified_chat_data = false; self.modified_display_name = false; } } @@ -596,6 +632,10 @@ pub(crate) fn update_player_list( actions.set_update_game_mode(true); } + if entry.chat_data.is_some() { + actions.set_initialize_chat(true); + } + if entry.display_name.is_some() { actions.set_update_display_name(true); } @@ -606,7 +646,7 @@ pub(crate) fn update_player_list( player_uuid: uuid, username: &entry.username, properties: Cow::Borrowed(&entry.properties), - chat_data: None, + chat_data: entry.chat_data.as_ref().map(|d| d.into()), listed: entry.listed, ping: entry.ping, game_mode: entry.game_mode, @@ -635,6 +675,11 @@ pub(crate) fn update_player_list( actions.set_update_latency(true); } + if entry.modified_chat_data { + entry.modified_chat_data = false; + actions.set_initialize_chat(true); + } + if entry.modified_display_name { entry.modified_display_name = false; actions.set_update_display_name(true); @@ -647,7 +692,7 @@ pub(crate) fn update_player_list( player_uuid: uuid, username: &entry.username, properties: Cow::default(), - chat_data: None, + chat_data: entry.chat_data.as_ref().map(|d| d.into()), listed: entry.listed, ping: entry.ping, game_mode: entry.game_mode, diff --git a/crates/valence_protocol/src/packet/c2s/play/chat_message.rs b/crates/valence_protocol/src/packet/c2s/play/chat_message.rs index 1e23ace06..948522211 100644 --- a/crates/valence_protocol/src/packet/c2s/play/chat_message.rs +++ b/crates/valence_protocol/src/packet/c2s/play/chat_message.rs @@ -7,7 +7,7 @@ pub struct ChatMessageC2s<'a> { pub timestamp: u64, pub salt: u64, pub signature: Option<&'a [u8; 256]>, - pub message_count: VarInt, + pub message_index: VarInt, // This is a bitset of 20; each bit represents one // of the last 20 messages received and whether or not // the message was acknowledged by the client diff --git a/crates/valence_protocol/src/packet/c2s/play/message_acknowledgment.rs b/crates/valence_protocol/src/packet/c2s/play/message_acknowledgment.rs index dff024545..1ada394cc 100644 --- a/crates/valence_protocol/src/packet/c2s/play/message_acknowledgment.rs +++ b/crates/valence_protocol/src/packet/c2s/play/message_acknowledgment.rs @@ -3,5 +3,5 @@ use crate::{Decode, Encode}; #[derive(Copy, Clone, Debug, Encode, Decode)] pub struct MessageAcknowledgmentC2s { - pub message_count: VarInt, + pub message_index: VarInt, } diff --git a/crates/valence_protocol/src/packet/c2s/play/player_session.rs b/crates/valence_protocol/src/packet/c2s/play/player_session.rs index e46e2fc87..79e0c7897 100644 --- a/crates/valence_protocol/src/packet/c2s/play/player_session.rs +++ b/crates/valence_protocol/src/packet/c2s/play/player_session.rs @@ -1,12 +1,29 @@ +use std::borrow::Cow; + use uuid::Uuid; use crate::{Decode, Encode}; -#[derive(Copy, Clone, Debug, Encode, Decode)] -pub struct PlayerSessionC2s<'a> { +#[derive(Clone, Debug, Encode, Decode)] +pub struct PlayerSessionC2s<'a>(pub Cow<'a, PlayerSessionData>); + +#[derive(Clone, PartialEq, Debug, Encode, Decode)] +pub struct PlayerSessionData { pub session_id: Uuid, // Public key pub expires_at: i64, - pub public_key_data: &'a [u8], - pub key_signature: &'a [u8], + pub public_key_data: Box<[u8]>, + pub key_signature: Box<[u8]>, +} + +impl<'a> From for Cow<'a, PlayerSessionData> { + fn from(value: PlayerSessionData) -> Self { + Cow::Owned(value) + } +} + +impl<'a> From<&'a PlayerSessionData> for Cow<'a, PlayerSessionData> { + fn from(value: &'a PlayerSessionData) -> Self { + Cow::Borrowed(value) + } } diff --git a/crates/valence_protocol/src/packet/s2c/play/player_list.rs b/crates/valence_protocol/src/packet/s2c/play/player_list.rs index 0af77b46d..78c62ff8b 100644 --- a/crates/valence_protocol/src/packet/s2c/play/player_list.rs +++ b/crates/valence_protocol/src/packet/s2c/play/player_list.rs @@ -4,6 +4,7 @@ use std::io::Write; use bitfield_struct::bitfield; use uuid::Uuid; +use crate::packet::c2s::play::player_session::PlayerSessionData; use crate::text::Text; use crate::types::{GameMode, Property}; use crate::var_int::VarInt; @@ -32,22 +33,13 @@ pub struct Entry<'a> { pub player_uuid: Uuid, pub username: &'a str, pub properties: Cow<'a, [Property]>, - pub chat_data: Option>, + pub chat_data: Option>, pub listed: bool, pub ping: i32, pub game_mode: GameMode, pub display_name: Option>, } -#[derive(Clone, PartialEq, Debug, Encode, Decode)] -pub struct ChatData<'a> { - pub session_id: Uuid, - /// Unix timestamp in milliseconds. - pub key_expiry_time: i64, - pub public_key: &'a [u8], - pub public_key_signature: &'a [u8], -} - impl<'a> Encode for PlayerListS2c<'a> { fn encode(&self, mut w: impl Write) -> anyhow::Result<()> { self.actions.0.encode(&mut w)?; From 06112408e78ed7ef3aa5b8bee3550d20ed976e02 Mon Sep 17 00:00:00 2001 From: guac420 <60993440+guac42@users.noreply.github.com> Date: Wed, 15 Mar 2023 00:13:42 -0700 Subject: [PATCH 02/32] Working chat verification - added chat type registry (required for sending ChatMessageS2c packets) - made some minor changes in valence_protocol (reworked how some types are structured in order to make invalid data states impossible) - finished a very, very rough example implementation of chat verification (still needs to broadcast packets to all clients) --- crates/valence/Cargo.toml | 2 +- crates/valence/examples/chat.rs | 374 ++++++++++++++---- crates/valence/src/chat_type.rs | 190 +++++++++ crates/valence/src/config.rs | 22 ++ crates/valence/src/lib.rs | 1 + crates/valence/src/server.rs | 45 ++- .../src/packet/s2c/play/chat_message.rs | 58 +-- crates/valence_protocol/src/types.rs | 32 +- 8 files changed, 589 insertions(+), 135 deletions(-) create mode 100644 crates/valence/src/chat_type.rs diff --git a/crates/valence/Cargo.toml b/crates/valence/Cargo.toml index f4c28ee1f..87d060606 100644 --- a/crates/valence/Cargo.toml +++ b/crates/valence/Cargo.toml @@ -33,7 +33,7 @@ rustc-hash = "1.1.0" serde = { version = "1.0.145", features = ["derive"] } serde_json = "1.0.85" sha1 = { version = "0.10.5", features = ["oid"] } -sha2 = "0.10.6" +sha2 = { version = "0.10.6", features = ["oid"] } thiserror = "1.0.35" tokio = { version = "1.25.0", features = ["full"] } tracing = "0.1.37" diff --git a/crates/valence/examples/chat.rs b/crates/valence/examples/chat.rs index c6307a1a8..aa2982136 100644 --- a/crates/valence/examples/chat.rs +++ b/crates/valence/examples/chat.rs @@ -1,10 +1,12 @@ +use std::collections::{HashMap, HashSet, VecDeque}; use std::time::SystemTime; use bevy_app::App; use rsa::pkcs8::DecodePublicKey; use rsa::{PaddingScheme, PublicKey, RsaPublicKey}; use sha1::{Digest, Sha1}; -use tracing::{debug, info, Level, warn}; +use sha2::Sha256; +use tracing::{debug, info, warn, Level}; use valence::client::despawn_disconnected_clients; use valence::client::event::{ default_event_handler, ChatMessage, ClientSettings, CommandExecution, MessageAcknowledgment, @@ -12,11 +14,16 @@ use valence::client::event::{ }; use valence::prelude::*; use valence::protocol::packet::c2s::play::client_settings::ChatMode; +use valence::protocol::packet::s2c::play::chat_message::{ChatMessageS2c, MessageFilterType}; use valence::protocol::translation_key::{ - CHAT_DISABLED_OPTIONS, MULTIPLAYER_DISCONNECT_CHAT_VALIDATION_FAILED, - MULTIPLAYER_DISCONNECT_EXPIRED_PUBLIC_KEY, MULTIPLAYER_DISCONNECT_INVALID_PUBLIC_KEY_SIGNATURE, - MULTIPLAYER_DISCONNECT_OUT_OF_ORDER_CHAT, + CHAT_DISABLED_CHAIN_BROKEN, CHAT_DISABLED_EXPIRED_PROFILE_KEY, + CHAT_DISABLED_MISSING_PROFILE_KEY, CHAT_DISABLED_OPTIONS, + MULTIPLAYER_DISCONNECT_CHAT_VALIDATION_FAILED, MULTIPLAYER_DISCONNECT_EXPIRED_PUBLIC_KEY, + MULTIPLAYER_DISCONNECT_INVALID_PUBLIC_KEY_SIGNATURE, MULTIPLAYER_DISCONNECT_OUT_OF_ORDER_CHAT, + MULTIPLAYER_DISCONNECT_TOO_MANY_PENDING_CHATS, MULTIPLAYER_DISCONNECT_UNSIGNED_CHAT, }; +use valence::protocol::types::MessageSignature; +use valence::protocol::var_int::VarInt; const SPAWN_Y: i32 = 64; @@ -27,22 +34,84 @@ struct MojangServicesState { public_key: RsaPublicKey, } -#[derive(Debug, Default)] +#[derive(Component, Debug)] +struct ChatState { + last_message_timestamp: u64, + chat_mode: ChatMode, + validator: AcknowledgementValidator, + chain: MessageChain, + signature_storage: MessageSignatureStorage, + session: Option, +} + +#[derive(Clone, Debug)] struct AcknowledgementValidator { messages: Vec>, - last_signature: Option>, + last_signature: Option<[u8; 256]>, } -#[derive(Debug)] +#[derive(Clone, Debug)] struct AcknowledgedMessage { - pub signature: Box<[u8; 256]>, + pub signature: [u8; 256], pub pending: bool, } +#[derive(Clone, Debug)] +struct MessageChain { + link: Option, +} + +#[derive(Copy, Clone, Debug)] +struct MessageLink { + index: i32, + sender: Uuid, + session_id: Uuid, +} + +#[derive(Clone, Debug)] +struct MessageSignatureStorage { + signatures: [Option<[u8; 256]>; 128], + indices: HashMap<[u8; 256], i32>, +} + +#[derive(Clone, Debug)] +struct ChatSession { + expires_at: i64, + public_key: RsaPublicKey, +} + +impl ChatState { + pub fn add_pending(&mut self, last_seen: &mut VecDeque<[u8; 256]>, signature: &[u8; 256]) { + self.signature_storage.add(last_seen, signature); + self.validator.add_pending(signature); + } +} + impl AcknowledgementValidator { + pub fn new() -> Self { + Self { + messages: vec![None; 20], + last_signature: None, + } + } + + pub fn add_pending(&mut self, signature: &[u8; 256]) { + if matches!(&self.last_signature, Some(last_sig) if signature == last_sig.as_ref()) { + return; + } + self.messages.push(Some(AcknowledgedMessage { + signature: *signature, + pending: true, + })); + self.last_signature = Some(*signature); + } + pub fn remove_until(&mut self, index: i32) -> bool { if index >= 0 && index <= (self.messages.len() - 20) as i32 { self.messages.drain(0..index as usize); + if self.messages.len() < 20 { + warn!("Message validator 'messages' shrunk!"); + } return true; } false @@ -52,7 +121,7 @@ impl AcknowledgementValidator { &mut self, acknowledgements: &[u8; 3], message_index: i32, - ) -> Option> { + ) -> Option> { if !self.remove_until(message_index) { // Invalid message index return None; @@ -71,46 +140,107 @@ impl AcknowledgementValidator { return None; } - let mut list = Vec::with_capacity(acknowledged_count); + let mut list = VecDeque::with_capacity(acknowledged_count); for i in 0..20 { let acknowledgement = acknowledgements[i >> 3] & (0b1 << (i % 8)) != 0; let acknowledged_message = unsafe { self.messages.get_unchecked_mut(i) }; + // Client has acknowledged the i-th message if acknowledgement { - // Client has acknowledged the i-th message + // The validator has the i-th message if let Some(m) = acknowledged_message { - // The validator has the i-th message m.pending = false; - list.push(*m.signature); + list.push_back(m.signature); } else { // Client has acknowledged a non-existing message + warn!("Client has acknowledged a non-existing message"); return None; } + } else { + // Client has not acknowledged the i-th message + if matches!(acknowledged_message, Some(m) if !m.pending) { + // The validator has an i-th message that has been validated but the client + // claims that it hasn't been validated yet + warn!( + "The validator has an i-th message that has been validated but the \ + clientclaims that it hasn't been validated yet" + ); + return None; + } + // Honestly not entirely sure why this is done + if acknowledged_message.is_some() { + *acknowledged_message = None; + } } - // Client has not acknowledged the i-th message - if matches!(acknowledged_message, Some(m) if !m.pending) { - // The validator has an i-th message that has already been validated - return None; + } + Some(list) + } + + pub fn message_count(&self) -> usize { + self.messages.len() + } +} + +impl MessageChain { + pub fn next_link(&mut self) -> Option { + match &mut self.link { + None => self.link, + Some(current) => { + let temp = *current; + current.index += 1; + Some(temp) } - // If the validator has doesn't have an i-th message or it is pending - unsafe { - let m = self.messages.get_unchecked_mut(i); - *m = None; + } + } +} + +impl MessageSignatureStorage { + fn index_of(&self, signature: &[u8; 256]) -> Option<&i32> { + self.indices.get(signature) + } + + /// Consumes [last_seen] + pub fn add(&mut self, last_seen: &mut VecDeque<[u8; 256]>, signature: &[u8; 256]) { + last_seen.push_back(*signature); + let mut sig_set = HashSet::new(); + for sig in last_seen.iter() { + sig_set.insert(*sig); + } + for i in 0..128 { + if last_seen.is_empty() { + return; + } + // Remove old message + let message_sig_data = self.signatures[i]; + // Add previously seen message + self.signatures[i] = last_seen.pop_back(); + if let Some(data) = self.signatures[i] { + self.indices.insert(data, i as i32); + } + // Reinsert old message if it is not already in last_seen + if let Some(data) = message_sig_data { + self.indices.remove(&data); + if sig_set.insert(data) { + last_seen.push_front(data); + } } } - Some(list) } } -#[derive(Component)] -struct ChatState { - last_message_timestamp: u64, - chat_mode: ChatMode, - validator: AcknowledgementValidator, - public_key: Option, +impl ChatSession { + pub fn is_expired(&self) -> bool { + SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .expect("Unable to get Unix time") + .as_millis() + >= self.expires_at as u128 + } } pub fn main() { - tracing_subscriber::fmt().with_max_level(Level::DEBUG).init(); + tracing_subscriber::fmt() + .with_max_level(Level::DEBUG) + .init(); App::new() .add_plugin(ServerPlugin::new(())) @@ -123,8 +253,9 @@ pub fn main() { handle_message_events, handle_message_acknowledgement, handle_command_events, + handle_chat_settings_event, ) - .in_schedule(EventLoopSchedule) + .in_schedule(EventLoopSchedule), ) .add_systems(PlayerList::default_systems()) .add_system(despawn_disconnected_clients) @@ -135,7 +266,9 @@ fn setup(mut commands: Commands, server: Res) { let mojang_pub_key = RsaPublicKey::from_public_key_der(MOJANG_KEY_DATA) .expect("Error creating Mojang public key"); - commands.insert_resource(MojangServicesState { public_key: mojang_pub_key }); + commands.insert_resource(MojangServicesState { + public_key: mojang_pub_key, + }); let mut instance = server.new_instance(DimensionId::default()); @@ -167,14 +300,19 @@ fn init_clients( client.send_message("Welcome to Valence! Talk about something.".italic()); client.set_instance(instance); - let mut state = ChatState { + let state = ChatState { last_message_timestamp: SystemTime::now() .duration_since(SystemTime::UNIX_EPOCH) .expect("Unable to get Unix time") .as_millis() as u64, chat_mode: ChatMode::Enabled, - validator: Default::default(), - public_key: None, + validator: AcknowledgementValidator::new(), + chain: MessageChain { link: None }, + signature_storage: MessageSignatureStorage { + signatures: [None; 128], + indices: HashMap::new(), + }, + session: None, }; commands.entity(entity).insert(state); @@ -252,13 +390,26 @@ fn handle_session_events( continue; }; - if let Ok(public_key) = RsaPublicKey::from_public_key_der(session.session_data.public_key_data.as_ref()) { - state.public_key = Some(public_key); + if let Ok(public_key) = + RsaPublicKey::from_public_key_der(session.session_data.public_key_data.as_ref()) + { + state.chain.link = Some(MessageLink { + index: 0, + sender: client.uuid(), + session_id: session.session_data.session_id, + }); + state.session = Some(ChatSession { + expires_at: session.session_data.expires_at, + public_key, + }); } else { - // This shouldn't happen considering that it is highly unlikely that Mojang would - // provide the client with a malformed key. By this point the key signature has - // been verified - warn!("Received malformed profile key data from '{}'", client.username()); + // This shouldn't happen considering that it is highly unlikely that Mojang + // would provide the client with a malformed key. By this point the + // key signature has been verified + warn!( + "Received malformed profile key data from '{}'", + client.username() + ); client.kick(Text::translate( MULTIPLAYER_DISCONNECT_EXPIRED_PUBLIC_KEY, ["Malformed profile key data".color(Color::RED)], @@ -302,13 +453,10 @@ fn handle_message_acknowledgement( } fn handle_message_events( - player_list: ResMut, mut clients: Query<&mut Client>, mut states: Query<&mut ChatState>, mut messages: EventReader, ) { - let pl = player_list.into_inner(); - for message in messages.iter() { let Ok(mut client) = clients.get_component_mut::(message.client) else { warn!("Unable to find client for message '{:?}'", message); @@ -320,6 +468,7 @@ fn handle_message_events( continue; }; + // Ensure that the client isn't sending messages while their chat is hidden if state.chat_mode == ChatMode::Hidden { client.send_message(Text::translate(CHAT_DISABLED_OPTIONS, []).color(Color::RED)); continue; @@ -327,6 +476,7 @@ fn handle_message_events( let message_text = message.message.to_string(); + // Ensure we are receiving chat messages in order if message.timestamp < state.last_message_timestamp { warn!( "{:?} sent out-of-order chat: '{:?}'", @@ -342,39 +492,127 @@ fn handle_message_events( state.last_message_timestamp = message.timestamp; - let Some(player_entry) = pl.get_mut(client.uuid()) else { - warn!("Unable to find '{}' in the player list", client.username()); - continue; - }; - - match state.validator.validate(&message.acknowledgements, message.message_index) { - Some(last_seen) => { - // This process should probably be done on another thread similarly to chunk loading - // in the 'terrain.rs' example, as this is what the notchian server does - - } + // Validate the message acknowledgements + match state + .validator + .validate(&message.acknowledgements, message.message_index) + { None => { warn!( - "Failed to validate acknowledgements from {:?}", - client.username() + "Failed to validate acknowledgements from `{}`", + client.username().as_str() ); client.kick(MULTIPLAYER_DISCONNECT_CHAT_VALIDATION_FAILED); + continue; } - }; - - info!("{}: {}", client.username(), message_text); + Some(mut last_seen) => { + // This whole process should probably be done on another thread similarly to + // chunk loading in the 'terrain.rs' example, as this is what + // the notchian server does + + let Some(link) = &state.chain.next_link() else { + client.send_message(Text::translate( + CHAT_DISABLED_CHAIN_BROKEN, + [], + ).color(Color::RED)); + continue; + }; + + let Some(session) = &state.session else { + client.kick(Text::translate( + CHAT_DISABLED_MISSING_PROFILE_KEY, + [], + )); + continue; + }; + + if session.is_expired() { + client.send_message( + Text::translate(CHAT_DISABLED_EXPIRED_PROFILE_KEY, []).color(Color::RED), + ); + continue; + } - // ############################################ + let Some(message_signature) = &message.signature else { + client.kick(Text::translate( + MULTIPLAYER_DISCONNECT_UNSIGNED_CHAT, + [], + )); + continue; + }; + + let mut hasher = Sha256::new(); + hasher.update([0u8, 0, 0, 1]); + hasher.update(link.sender.into_bytes()); + hasher.update(link.session_id.into_bytes()); + hasher.update(link.index.to_be_bytes()); + hasher.update(message.salt.to_be_bytes()); + hasher.update((message.timestamp / 1000).to_be_bytes()); + let bytes = message.message.as_bytes(); + hasher.update((bytes.len() as u32).to_be_bytes()); + hasher.update(bytes); + hasher.update((last_seen.len() as u32).to_be_bytes()); + for sig in last_seen.iter() { + hasher.update(sig); + } + let hashed = hasher.finalize(); + + if session + .public_key + .verify( + PaddingScheme::new_pkcs1v15_sign::(), + &hashed, + message_signature.as_ref(), + ) + .is_err() + { + client.kick(Text::translate(MULTIPLAYER_DISCONNECT_UNSIGNED_CHAT, [])); + continue; + } - let formatted = format!("<{}>: ", client.username()) - .bold() - .color(Color::YELLOW) - + message_text.into_text().not_bold().color(Color::WHITE); + let previous = last_seen + .iter() + .map(|sig| match state.signature_storage.index_of(sig) { + Some(index) => MessageSignature::ByIndex(*index), + None => MessageSignature::BySignature(sig), + }) + .collect::>(); + + let packet = ChatMessageS2c { + sender: link.sender, + index: VarInt(link.index), + message_signature: Some(message_signature.as_ref()), + message: message.message.as_ref(), + time_stamp: message.timestamp, + salt: message.salt, + previous_messages: previous, + unsigned_content: None, + filter_type: MessageFilterType::PassThrough, + chat_type: VarInt(0), + network_name: client.username().into_text().into(), + network_target_name: None, + }; + + // TODO: clients.for_each + client.write_packet(&packet); + + // Add pending acknowledgement + state.add_pending(&mut last_seen, message_signature.as_ref()); + if state.validator.message_count() > 4096 { + client.kick(Text::translate( + MULTIPLAYER_DISCONNECT_TOO_MANY_PENDING_CHATS, + [], + )); + continue; + } + // clients.for_each - // TODO: write message to instance buffer. - for mut client in &mut clients { - client.send_message(formatted.clone()); + info!("{}: {}", client.username(), message_text); + } } + + // send chat message packets to each client and add pending + // acknowledgements for clients.iter_mut() } } @@ -411,7 +649,5 @@ fn handle_chat_settings_event( }; state.chat_mode = *chat_mode; - - debug!("Client settings: {:?}", chat_mode); } } diff --git a/crates/valence/src/chat_type.rs b/crates/valence/src/chat_type.rs new file mode 100644 index 000000000..f447c01bf --- /dev/null +++ b/crates/valence/src/chat_type.rs @@ -0,0 +1,190 @@ +//! ChatType configuration and identification. + +use std::collections::HashSet; + +use anyhow::ensure; +use valence_nbt::{compound, Compound, List}; +use valence_protocol::ident; +use valence_protocol::ident::Ident; +use valence_protocol::text::Color; + +/// Identifies a particular [`ChatType`] on the server. +/// +/// The default chat type ID refers to the first chat type added in +/// [`ServerPlugin::chat_types`]. +/// +/// To obtain chat type IDs for other chat types, see +/// [`ServerPlugin::chat_types`]. +/// +/// [`ServerPlugin::chat_types`]: crate::config::ServerPlugin::chat_types +#[derive(Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)] +pub struct ChatTypeId(pub(crate) u16); + +/// Contains the configuration for a chat type. +/// +/// Chat types are registered once at startup through +/// [`ServerPlugin::with_chat_types`] +/// +/// Note that [`ChatTypeDecoration::style`] for [`ChatType::narration`] +/// is unused by the notchian client and is ignored. +/// +/// [`ServerPlugin::with_chat_types`]: crate::config::ServerPlugin::with_chat_types +#[derive(Clone, Debug)] +pub struct ChatType { + pub name: Ident, + pub chat: ChatTypeDecoration, + pub narration: ChatTypeDecoration, +} + +impl ChatType { + pub(crate) fn to_chat_type_registry_item(&self, id: i32) -> Compound { + compound! { + "name" => self.name.clone(), + "id" => id, + "element" => compound! { + "chat" => { + let mut chat = compound! { + "translation_key" => self.chat.translation_key.clone(), + "parameters" => { + let mut parameters = Vec::new(); + if self.chat.parameters.sender { + parameters.push("sender".to_string()); + } + if self.chat.parameters.target { + parameters.push("target".to_string()); + } + if self.chat.parameters.content { + parameters.push("content".to_string()); + } + List::String(parameters) + }, + }; + if let Some(style) = &self.chat.style { + let mut s = Compound::new(); + if let Some(color) = style.color { + s.insert("color", format!("#{:02x}{:02x}{:02x}", color.r, color.g, color.b)); + } + if let Some(bold) = style.bold { + s.insert("bold", bold); + } + if let Some(italic) = style.italic { + s.insert("italic", italic); + } + if let Some(underlined) = style.underlined { + s.insert("underlined", underlined); + } + if let Some(strikethrough) = style.strikethrough { + s.insert("strikethrough", strikethrough); + } + if let Some(obfuscated) = style.obfuscated { + s.insert("obfuscated", obfuscated); + } + if let Some(insertion) = &style.insertion { + s.insert("insertion", insertion.clone()); + } + if let Some(font) = &style.font { + s.insert("font", font.clone()); + } + chat.insert("style", s); + } + chat + }, + "narration" => compound! { + "translation_key" => self.narration.translation_key.clone(), + "parameters" => { + let mut parameters = Vec::new(); + if self.narration.parameters.sender { + parameters.push("sender".into()); + } + if self.narration.parameters.target { + parameters.push("target".into()); + } + if self.narration.parameters.content { + parameters.push("content".into()); + } + List::String(parameters) + }, + }, + } + } + } +} + +pub(crate) fn validate_chat_types(chat_types: &[ChatType]) -> anyhow::Result<()> { + ensure!( + !chat_types.is_empty(), + "at least one chat type must be present" + ); + + ensure!( + chat_types.len() <= u16::MAX as _, + "more than u16::MAX chat types present" + ); + + let mut names = HashSet::new(); + + for chat_type in chat_types { + ensure!( + names.insert(chat_type.name.clone()), + "chat type \"{}\" already exists", + chat_type.name + ); + } + + Ok(()) +} + +impl Default for ChatType { + fn default() -> Self { + Self { + name: ident!("chat"), + chat: ChatTypeDecoration { + translation_key: "chat.type.text".into(), + style: None, + parameters: ChatTypeParameters { + sender: true, + content: true, + ..Default::default() + }, + }, + narration: ChatTypeDecoration { + translation_key: "chat.type.text.narrate".into(), + style: None, + parameters: ChatTypeParameters { + sender: true, + content: true, + ..Default::default() + }, + }, + } + } +} + +#[derive(Clone, PartialEq, Default, Debug)] +pub struct ChatTypeDecoration { + pub translation_key: String, + pub style: Option, + pub parameters: ChatTypeParameters, +} + +#[derive(Clone, PartialEq, Default, Debug)] +pub struct ChatTypeStyle { + pub color: Option, + pub bold: Option, + pub italic: Option, + pub underlined: Option, + pub strikethrough: Option, + pub obfuscated: Option, + pub insertion: Option, + pub font: Option>, + // TODO + // * click_event: Option, + // * hover_event: Option, +} + +#[derive(Clone, Copy, PartialEq, Eq, Default, Debug)] +pub struct ChatTypeParameters { + sender: bool, + target: bool, + content: bool, +} diff --git a/crates/valence/src/config.rs b/crates/valence/src/config.rs index a19d97753..e5b6eb48f 100644 --- a/crates/valence/src/config.rs +++ b/crates/valence/src/config.rs @@ -11,6 +11,7 @@ use valence_protocol::text::Text; use valence_protocol::username::Username; use crate::biome::Biome; +use crate::chat_type::ChatType; use crate::dimension::Dimension; use crate::server::{NewClientInfo, SharedServer}; @@ -132,6 +133,19 @@ pub struct ServerPlugin { /// /// `vec![Biome::default()]`. pub biomes: Arc<[Biome]>, + /// The list of [`ChatType`]s usable on the server. + /// + /// The chat types returned by [`ServerPlugin::chat_types`] will be in the + /// same order as this `Vec`. + /// + /// The number of elements in the `Vec` must be in `1..=u16::MAX`. + /// Additionally, the documented requirements on the fields of [`ChatType`] + /// must be met. + /// + /// # Default Value + /// + /// `vec![ChatType::default()]` + pub chat_types: Arc<[ChatType]>, } impl ServerPlugin { @@ -151,6 +165,7 @@ impl ServerPlugin { outgoing_capacity: 8388608, // 8 MiB dimensions: [Dimension::default()].as_slice().into(), biomes: [Biome::default()].as_slice().into(), + chat_types: [ChatType::default()].as_slice().into(), } } @@ -223,6 +238,13 @@ impl ServerPlugin { self.biomes = biomes.into(); self } + + /// See [`Self::chat_types`]. + #[must_use] + pub fn with_chat_types(mut self, chat_types: impl Into>) -> Self { + self.chat_types = chat_types.into(); + self + } } impl Default for ServerPlugin { diff --git a/crates/valence/src/lib.rs b/crates/valence/src/lib.rs index 68c10fe3a..bd1721059 100644 --- a/crates/valence/src/lib.rs +++ b/crates/valence/src/lib.rs @@ -32,6 +32,7 @@ pub use { }; pub mod biome; +pub mod chat_type; pub mod client; pub mod config; pub mod dimension; diff --git a/crates/valence/src/server.rs b/crates/valence/src/server.rs index 3475451a9..3a802c712 100644 --- a/crates/valence/src/server.rs +++ b/crates/valence/src/server.rs @@ -21,6 +21,7 @@ use valence_protocol::types::Property; use valence_protocol::username::Username; use crate::biome::{validate_biomes, Biome, BiomeId}; +use crate::chat_type::{validate_chat_types, ChatType, ChatTypeId}; use crate::client::event::{register_client_events, run_event_loop}; use crate::client::{update_clients, Client}; use crate::config::{AsyncCallbacks, ConnectionMode, ServerPlugin}; @@ -88,6 +89,7 @@ struct SharedServerInner { _tokio_runtime: Option, dimensions: Arc<[Dimension]>, biomes: Arc<[Biome]>, + chat_types: Arc<[ChatType]>, /// Contains info about dimensions, biomes, and chats. /// Sent to all clients when joining. registry_codec: Compound, @@ -193,6 +195,30 @@ impl SharedServer { .map(|(i, b)| (BiomeId(i as u16), b)) } + /// Obtains a [`ChatType`] by using its corresponding [`ChatTypeId`]. + #[track_caller] + pub fn chat_type(&self, id: ChatTypeId) -> &ChatType { + self.0 + .chat_types + .get(id.0 as usize) + .expect("invalid chat type ID") + } + + /// Returns an iterator over all added chat types and their associated + /// [`ChatTypeId`] in ascending order. + pub fn chat_types( + &self, + ) -> impl ExactSizeIterator + + DoubleEndedIterator + + FusedIterator + + Clone { + self.0 + .chat_types + .iter() + .enumerate() + .map(|(i, t)| (ChatTypeId(i as u16), t)) + } + pub(crate) fn registry_codec(&self) -> &Compound { &self.0.registry_codec } @@ -248,8 +274,10 @@ pub fn build_plugin( validate_dimensions(&plugin.dimensions)?; validate_biomes(&plugin.biomes)?; + validate_chat_types(&plugin.chat_types)?; - let registry_codec = make_registry_codec(&plugin.dimensions, &plugin.biomes); + let registry_codec = + make_registry_codec(&plugin.dimensions, &plugin.biomes, &plugin.chat_types); let (new_clients_send, new_clients_recv) = flume::bounded(64); @@ -265,6 +293,7 @@ pub fn build_plugin( _tokio_runtime: runtime, dimensions: plugin.dimensions.clone(), biomes: plugin.biomes.clone(), + chat_types: plugin.chat_types.clone(), registry_codec, new_clients_send, new_clients_recv, @@ -383,7 +412,11 @@ fn increment_tick_counter(mut server: ResMut) { server.current_tick += 1; } -fn make_registry_codec(dimensions: &[Dimension], biomes: &[Biome]) -> Compound { +fn make_registry_codec( + dimensions: &[Dimension], + biomes: &[Biome], + chat_types: &[ChatType], +) -> Compound { let dimensions = dimensions .iter() .enumerate() @@ -396,6 +429,12 @@ fn make_registry_codec(dimensions: &[Dimension], biomes: &[Biome]) -> Compound { .map(|(id, biome)| biome.to_biome_registry_item(id as i32)) .collect(); + let chat_types = chat_types + .iter() + .enumerate() + .map(|(id, chat_type)| chat_type.to_chat_type_registry_item(id as i32)) + .collect(); + compound! { ident!("dimension_type") => compound! { "type" => ident!("dimension_type"), @@ -407,7 +446,7 @@ fn make_registry_codec(dimensions: &[Dimension], biomes: &[Biome]) -> Compound { }, ident!("chat_type") => compound! { "type" => ident!("chat_type"), - "value" => List::Compound(vec![]), + "value" => List::Compound(chat_types), }, } } diff --git a/crates/valence_protocol/src/packet/s2c/play/chat_message.rs b/crates/valence_protocol/src/packet/s2c/play/chat_message.rs index 65f3f88de..3700b6d97 100644 --- a/crates/valence_protocol/src/packet/s2c/play/chat_message.rs +++ b/crates/valence_protocol/src/packet/s2c/play/chat_message.rs @@ -19,17 +19,16 @@ pub struct ChatMessageS2c<'a> { pub previous_messages: Vec>, pub unsigned_content: Option>, pub filter_type: MessageFilterType, - pub filter_type_bits: Option, pub chat_type: VarInt, pub network_name: Cow<'a, Text>, pub network_target_name: Option>, } -#[derive(Copy, Clone, PartialEq, Eq, Debug, Encode, Decode)] +#[derive(Clone, PartialEq, Eq, Debug, Encode, Decode)] pub enum MessageFilterType { PassThrough, FullyFiltered, - PartiallyFiltered, + PartiallyFiltered { mask: Vec }, } impl<'a> Encode for ChatMessageS2c<'a> { @@ -43,15 +42,6 @@ impl<'a> Encode for ChatMessageS2c<'a> { self.previous_messages.encode(&mut w)?; self.unsigned_content.encode(&mut w)?; self.filter_type.encode(&mut w)?; - - if self.filter_type == MessageFilterType::PartiallyFiltered { - match self.filter_type_bits { - // Filler data - None => 0u8.encode(&mut w)?, - Some(bits) => bits.encode(&mut w)?, - } - } - self.chat_type.encode(&mut w)?; self.network_name.encode(&mut w)?; self.network_target_name.encode(&mut w)?; @@ -62,39 +52,19 @@ impl<'a> Encode for ChatMessageS2c<'a> { impl<'a> Decode<'a> for ChatMessageS2c<'a> { fn decode(r: &mut &'a [u8]) -> anyhow::Result { - let sender = Uuid::decode(r)?; - let index = VarInt::decode(r)?; - let message_signature = Option::<&'a [u8; 256]>::decode(r)?; - let message = <&str>::decode(r)?; - let time_stamp = u64::decode(r)?; - let salt = u64::decode(r)?; - let previous_messages = Vec::::decode(r)?; - let unsigned_content = Option::>::decode(r)?; - let filter_type = MessageFilterType::decode(r)?; - - let filter_type_bits = match filter_type { - MessageFilterType::PartiallyFiltered => Some(u8::decode(r)?), - _ => None, - }; - - let chat_type = VarInt::decode(r)?; - let network_name = >::decode(r)?; - let network_target_name = Option::>::decode(r)?; - Ok(Self { - sender, - index, - message_signature, - message, - time_stamp, - salt, - previous_messages, - unsigned_content, - filter_type, - filter_type_bits, - chat_type, - network_name, - network_target_name, + sender: Uuid::decode(r)?, + index: VarInt::decode(r)?, + message_signature: Option::<&'a [u8; 256]>::decode(r)?, + message: <&str>::decode(r)?, + time_stamp: u64::decode(r)?, + salt: u64::decode(r)?, + previous_messages: Vec::::decode(r)?, + unsigned_content: Option::>::decode(r)?, + filter_type: MessageFilterType::decode(r)?, + chat_type: VarInt::decode(r)?, + network_name: >::decode(r)?, + network_target_name: Option::>::decode(r)?, }) } } diff --git a/crates/valence_protocol/src/types.rs b/crates/valence_protocol/src/types.rs index 96fd90a5b..7c67df24c 100644 --- a/crates/valence_protocol/src/types.rs +++ b/crates/valence_protocol/src/types.rs @@ -112,18 +112,19 @@ pub enum Direction { } #[derive(Copy, Clone, PartialEq, Debug)] -pub struct MessageSignature<'a> { - pub message_id: i32, - pub signature: Option<&'a [u8; 256]>, +pub enum MessageSignature<'a> { + ByIndex(i32), + BySignature(&'a [u8; 256]), } impl<'a> Encode for MessageSignature<'a> { fn encode(&self, mut w: impl Write) -> anyhow::Result<()> { - VarInt(self.message_id + 1).encode(&mut w)?; - - match self.signature { - None => {} - Some(signature) => signature.encode(&mut w)?, + match self { + MessageSignature::ByIndex(index) => VarInt(index + 1).encode(&mut w)?, + MessageSignature::BySignature(signature) => { + VarInt(0).encode(&mut w)?; + signature.encode(&mut w)?; + } } Ok(()) @@ -132,17 +133,12 @@ impl<'a> Encode for MessageSignature<'a> { impl<'a> Decode<'a> for MessageSignature<'a> { fn decode(r: &mut &'a [u8]) -> anyhow::Result { - let message_id = VarInt::decode(r)?.0 - 1; // TODO: this can underflow. + let index = VarInt::decode(r)?.0.saturating_sub(1); - let signature = if message_id == -1 { - Some(<&[u8; 256]>::decode(r)?) + if index == -1 { + Ok(MessageSignature::BySignature(<&[u8; 256]>::decode(r)?)) } else { - None - }; - - Ok(Self { - message_id, - signature, - }) + Ok(MessageSignature::ByIndex(index)) + } } } From bba93bfaa9c5b51f0d35f2bf59237f33306bfc38 Mon Sep 17 00:00:00 2001 From: guac420 <60993440+guac42@users.noreply.github.com> Date: Wed, 22 Mar 2023 20:50:46 -0700 Subject: [PATCH 03/32] Complete working example --- crates/valence/examples/chat.rs | 76 ++++++++++++++++++++------------- 1 file changed, 46 insertions(+), 30 deletions(-) diff --git a/crates/valence/examples/chat.rs b/crates/valence/examples/chat.rs index aa2982136..02c168ccf 100644 --- a/crates/valence/examples/chat.rs +++ b/crates/valence/examples/chat.rs @@ -193,6 +193,14 @@ impl MessageChain { } } +impl MessageLink { + pub fn update_hash(&self, hasher: &mut impl Digest) { + hasher.update(self.sender.into_bytes()); + hasher.update(self.session_id.into_bytes()); + hasher.update(self.index.to_be_bytes()); + } +} + impl MessageSignatureStorage { fn index_of(&self, signature: &[u8; 256]) -> Option<&i32> { self.indices.get(signature) @@ -367,8 +375,8 @@ fn handle_session_events( hasher.update(&serialized); let hash = hasher.finalize(); - // Verify the session key signature using the hashed session data and the Mojang - // public key + // Verify the session data using Mojang's public key and the hashed session data + // against the message signature if services_state .public_key .verify( @@ -385,14 +393,17 @@ fn handle_session_events( )); } + // Get the player's chat state let Ok(mut state) = states.get_component_mut::(session.client) else { warn!("Unable to find chat state for client '{:?}'", client.username()); continue; }; + // Decode the player's session public key from the data if let Ok(public_key) = RsaPublicKey::from_public_key_der(session.session_data.public_key_data.as_ref()) { + // Update the player's chat state data with the new player session data state.chain.link = Some(MessageLink { index: 0, sender: client.uuid(), @@ -416,6 +427,8 @@ fn handle_session_events( )); } + // Update the player list with the new session data + // The player list will then send this new session data to the other clients player_entry.set_chat_data(Some(session.session_data.clone())); } } @@ -456,7 +469,10 @@ fn handle_message_events( mut clients: Query<&mut Client>, mut states: Query<&mut ChatState>, mut messages: EventReader, + mut instances: Query<&mut Instance>, ) { + let mut instance = instances.single_mut(); + for message in messages.iter() { let Ok(mut client) = clients.get_component_mut::(message.client) else { warn!("Unable to find client for message '{:?}'", message); @@ -474,14 +490,12 @@ fn handle_message_events( continue; } - let message_text = message.message.to_string(); - // Ensure we are receiving chat messages in order if message.timestamp < state.last_message_timestamp { warn!( "{:?} sent out-of-order chat: '{:?}'", client.username(), - message_text + message.message.as_ref() ); client.kick(Text::translate( MULTIPLAYER_DISCONNECT_OUT_OF_ORDER_CHAT, @@ -526,6 +540,7 @@ fn handle_message_events( continue; }; + // Verify that the player's session has not expired if session.is_expired() { client.send_message( Text::translate(CHAT_DISABLED_EXPIRED_PROFILE_KEY, []).color(Color::RED), @@ -533,6 +548,7 @@ fn handle_message_events( continue; } + // Verify that the chat message is signed let Some(message_signature) = &message.signature else { client.kick(Text::translate( MULTIPLAYER_DISCONNECT_UNSIGNED_CHAT, @@ -541,11 +557,13 @@ fn handle_message_events( continue; }; - let mut hasher = Sha256::new(); - hasher.update([0u8, 0, 0, 1]); - hasher.update(link.sender.into_bytes()); - hasher.update(link.session_id.into_bytes()); - hasher.update(link.index.to_be_bytes()); + // Create the hash digest used to verify the chat message + let mut hasher = Sha256::new_with_prefix([0u8, 0, 0, 1]); + + // Update the hash with the player's message chain state + link.update_hash(&mut hasher); + + // Update the hash with the message contents hasher.update(message.salt.to_be_bytes()); hasher.update((message.timestamp / 1000).to_be_bytes()); let bytes = message.message.as_bytes(); @@ -557,6 +575,8 @@ fn handle_message_events( } let hashed = hasher.finalize(); + // Verify the chat message using the player's session public key and hashed data + // against the message signature if session .public_key .verify( @@ -570,6 +590,7 @@ fn handle_message_events( continue; } + // Create a list of messages that have been seen by the client let previous = last_seen .iter() .map(|sig| match state.signature_storage.index_of(sig) { @@ -578,7 +599,9 @@ fn handle_message_events( }) .collect::>(); - let packet = ChatMessageS2c { + info!("{}: {}", client.username(), message.message.as_ref()); + + instance.write_packet(&ChatMessageS2c { sender: link.sender, index: VarInt(link.index), message_signature: Some(message_signature.as_ref()), @@ -591,28 +614,21 @@ fn handle_message_events( chat_type: VarInt(0), network_name: client.username().into_text().into(), network_target_name: None, - }; - - // TODO: clients.for_each - client.write_packet(&packet); - - // Add pending acknowledgement - state.add_pending(&mut last_seen, message_signature.as_ref()); - if state.validator.message_count() > 4096 { - client.kick(Text::translate( - MULTIPLAYER_DISCONNECT_TOO_MANY_PENDING_CHATS, - [], - )); - continue; + }); + + for mut client in clients.iter_mut() { + // Add pending acknowledgement + state.add_pending(&mut last_seen, message_signature.as_ref()); + if state.validator.message_count() > 4096 { + client.kick(Text::translate( + MULTIPLAYER_DISCONNECT_TOO_MANY_PENDING_CHATS, + [], + )); + continue; + } } - // clients.for_each - - info!("{}: {}", client.username(), message_text); } } - - // send chat message packets to each client and add pending - // acknowledgements for clients.iter_mut() } } From 3b4f3a0af5fe506b05573b35f307bd133f5a33bb Mon Sep 17 00:00:00 2001 From: guac420 <60993440+guac42@users.noreply.github.com> Date: Fri, 24 Mar 2023 01:02:01 -0700 Subject: [PATCH 04/32] Fix merge conflicts --- crates/valence/examples/chat.rs | 234 +++++++++++++++++--------------- crates/valence/src/server.rs | 109 ++++++--------- 2 files changed, 167 insertions(+), 176 deletions(-) diff --git a/crates/valence/examples/chat.rs b/crates/valence/examples/chat.rs index 02c168ccf..d0a5183dc 100644 --- a/crates/valence/examples/chat.rs +++ b/crates/valence/examples/chat.rs @@ -1,17 +1,16 @@ use std::collections::{HashMap, HashSet, VecDeque}; use std::time::SystemTime; -use bevy_app::App; use rsa::pkcs8::DecodePublicKey; use rsa::{PaddingScheme, PublicKey, RsaPublicKey}; use sha1::{Digest, Sha1}; use sha2::Sha256; use tracing::{debug, info, warn, Level}; -use valence::client::despawn_disconnected_clients; use valence::client::event::{ - default_event_handler, ChatMessage, ClientSettings, CommandExecution, MessageAcknowledgment, - PlayerSession, + ChatMessage, ClientSettings, CommandExecution, MessageAcknowledgment, PlayerSession, }; +use valence::client::{default_event_handler, despawn_disconnected_clients}; +use valence::entity::player::PlayerBundle; use valence::prelude::*; use valence::protocol::packet::c2s::play::client_settings::ChatMode; use valence::protocol::packet::s2c::play::chat_message::{ChatMessageS2c, MessageFilterType}; @@ -95,6 +94,7 @@ impl AcknowledgementValidator { } } + /// Add a message pending acknowledgement via its `signature`. pub fn add_pending(&mut self, signature: &[u8; 256]) { if matches!(&self.last_signature, Some(last_sig) if signature == last_sig.as_ref()) { return; @@ -106,7 +106,13 @@ impl AcknowledgementValidator { self.last_signature = Some(*signature); } + /// Removes message signatures from the validator before an `index`. + /// + /// Message signatures will only be removed if the result leaves the + /// validator with at least 20 messages. Returns `true` if messages are + /// removed and `false` if they are not. pub fn remove_until(&mut self, index: i32) -> bool { + // Ensure that there will still be 20 messages in the array if index >= 0 && index <= (self.messages.len() - 20) as i32 { self.messages.drain(0..index as usize); if self.messages.len() < 20 { @@ -117,6 +123,9 @@ impl AcknowledgementValidator { false } + /// Validate a set of `acknowledgements` offset by `message_index`. + /// + /// Returns a [`VecDeque`] of acknowledged message signatures if the `acknowledgements` are valid and `None` if they are invalid. pub fn validate( &mut self, acknowledgements: &[u8; 3], @@ -143,6 +152,7 @@ impl AcknowledgementValidator { let mut list = VecDeque::with_capacity(acknowledged_count); for i in 0..20 { let acknowledgement = acknowledgements[i >> 3] & (0b1 << (i % 8)) != 0; + // SAFETY: The length of messages is never less than 20 let acknowledged_message = unsafe { self.messages.get_unchecked_mut(i) }; // Client has acknowledged the i-th message if acknowledgement { @@ -161,8 +171,8 @@ impl AcknowledgementValidator { // The validator has an i-th message that has been validated but the client // claims that it hasn't been validated yet warn!( - "The validator has an i-th message that has been validated but the \ - clientclaims that it hasn't been validated yet" + "The validator has an i-th message that has been validated but the client \ + claims that it hasn't been validated yet" ); return None; } @@ -175,6 +185,7 @@ impl AcknowledgementValidator { Some(list) } + /// The number of pending messages in the validator. pub fn message_count(&self) -> usize { self.messages.len() } @@ -202,11 +213,14 @@ impl MessageLink { } impl MessageSignatureStorage { - fn index_of(&self, signature: &[u8; 256]) -> Option<&i32> { - self.indices.get(signature) + /// Get the index of the `signature` in the storage if it exists. + pub fn index_of(&self, signature: &[u8; 256]) -> Option { + self.indices.get(signature).copied() } - /// Consumes [last_seen] + /// Update the signature storage according to `last_seen` while adding `signature` to the storage. + /// + /// Warning: this consumes `last_seen`. pub fn add(&mut self, last_seen: &mut VecDeque<[u8; 256]>, signature: &[u8; 256]) { last_seen.push_back(*signature); let mut sig_set = HashSet::new(); @@ -296,56 +310,58 @@ fn setup(mut commands: Commands, server: Res) { } fn init_clients( - mut commands: Commands, - mut clients: Query<(Entity, &mut Client), Added>, + mut clients: Query<(Entity, &UniqueId, &Username, &mut Client, &mut GameMode), Added>, instances: Query>, + mut commands: Commands, ) { - let instance = instances.single(); - - for (entity, mut client) in &mut clients { - client.set_position([0.0, SPAWN_Y as f64 + 1.0, 0.0]); - client.set_game_mode(GameMode::Adventure); + for (entity, uuid, username, mut client, mut game_mode) in &mut clients { + *game_mode = GameMode::Adventure; client.send_message("Welcome to Valence! Talk about something.".italic()); - client.set_instance(instance); - - let state = ChatState { - last_message_timestamp: SystemTime::now() - .duration_since(SystemTime::UNIX_EPOCH) - .expect("Unable to get Unix time") - .as_millis() as u64, - chat_mode: ChatMode::Enabled, - validator: AcknowledgementValidator::new(), - chain: MessageChain { link: None }, - signature_storage: MessageSignatureStorage { - signatures: [None; 128], - indices: HashMap::new(), - }, - session: None, - }; - commands.entity(entity).insert(state); + commands + .entity(entity) + .insert(PlayerBundle { + location: Location(instances.single()), + position: Position::new([0.0, SPAWN_Y as f64 + 1.0, 0.0]), + uuid: *uuid, + ..Default::default() + }) + .insert(ChatState { + last_message_timestamp: SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .expect("Unable to get Unix time") + .as_millis() as u64, + chat_mode: ChatMode::Enabled, + validator: AcknowledgementValidator::new(), + chain: MessageChain { link: None }, + signature_storage: MessageSignatureStorage { + signatures: [None; 128], + indices: HashMap::new(), + }, + session: None, + }); - info!("{} logged in!", client.username()); + info!("{} logged in!", username.0); } } fn handle_session_events( services_state: Res, player_list: ResMut, - mut clients: Query<&mut Client>, - mut states: Query<&mut ChatState>, + mut clients: Query<(&UniqueId, &Username, &mut ChatState)>, mut sessions: EventReader, + mut commands: Commands, ) { let pl = player_list.into_inner(); for session in sessions.iter() { - let Ok(mut client) = clients.get_component_mut::(session.client) else { + let Ok((uuid, username, mut state)) = clients.get_mut(session.client) else { warn!("Unable to find client for session"); continue; }; - let Some(player_entry) = pl.get_mut(client.uuid()) else { - warn!("Unable to find '{}' in the player list", client.username()); + let Some(player_entry) = pl.get_mut(uuid.0) else { + warn!("Unable to find '{}' in the player list", username.0); continue; }; @@ -357,16 +373,16 @@ fn handle_session_events( >= session.session_data.expires_at as u128 { warn!("Failed to validate profile key: expired public key"); - client.kick(Text::translate( - MULTIPLAYER_DISCONNECT_EXPIRED_PUBLIC_KEY, - [], - )); + commands.add(DisconnectClient { + client: session.client, + reason: Text::translate(MULTIPLAYER_DISCONNECT_EXPIRED_PUBLIC_KEY, []), + }); continue; } // Serialize the session data let mut serialized = Vec::with_capacity(318); - serialized.extend_from_slice(client.uuid().into_bytes().as_slice()); + serialized.extend_from_slice(uuid.0.into_bytes().as_slice()); serialized.extend_from_slice(session.session_data.expires_at.to_be_bytes().as_ref()); serialized.extend_from_slice(session.session_data.public_key_data.as_ref()); @@ -387,18 +403,12 @@ fn handle_session_events( .is_err() { warn!("Failed to validate profile key: invalid public key signature"); - client.kick(Text::translate( - MULTIPLAYER_DISCONNECT_INVALID_PUBLIC_KEY_SIGNATURE, - [], - )); + commands.add(DisconnectClient { + client: session.client, + reason: Text::translate(MULTIPLAYER_DISCONNECT_INVALID_PUBLIC_KEY_SIGNATURE, []), + }); } - // Get the player's chat state - let Ok(mut state) = states.get_component_mut::(session.client) else { - warn!("Unable to find chat state for client '{:?}'", client.username()); - continue; - }; - // Decode the player's session public key from the data if let Ok(public_key) = RsaPublicKey::from_public_key_der(session.session_data.public_key_data.as_ref()) @@ -406,7 +416,7 @@ fn handle_session_events( // Update the player's chat state data with the new player session data state.chain.link = Some(MessageLink { index: 0, - sender: client.uuid(), + sender: uuid.0, session_id: session.session_data.session_id, }); state.session = Some(ChatSession { @@ -417,14 +427,14 @@ fn handle_session_events( // This shouldn't happen considering that it is highly unlikely that Mojang // would provide the client with a malformed key. By this point the // key signature has been verified - warn!( - "Received malformed profile key data from '{}'", - client.username() - ); - client.kick(Text::translate( - MULTIPLAYER_DISCONNECT_EXPIRED_PUBLIC_KEY, - ["Malformed profile key data".color(Color::RED)], - )); + warn!("Received malformed profile key data from '{}'", username.0); + commands.add(DisconnectClient { + client: session.client, + reason: Text::translate( + MULTIPLAYER_DISCONNECT_EXPIRED_PUBLIC_KEY, + ["Malformed profile key data".color(Color::RED)], + ), + }); } // Update the player list with the new session data @@ -434,53 +444,49 @@ fn handle_session_events( } fn handle_message_acknowledgement( - mut clients: Query<&mut Client>, - mut states: Query<&mut ChatState>, + mut clients: Query<(&Username, &mut ChatState)>, mut acknowledgements: EventReader, + mut commands: Commands, ) { for acknowledgement in acknowledgements.iter() { - let Ok(mut client) = clients.get_component_mut::(acknowledgement.client) else { + let Ok((username, mut state)) = clients.get_mut(acknowledgement.client) else { warn!("Unable to find client for acknowledgement"); continue; }; - let Ok(mut state) = states.get_component_mut::(acknowledgement.client) else { - warn!("Unable to find chat state for client '{:?}'", client.username()); - continue; - }; - if !state.validator.remove_until(acknowledgement.message_index) { warn!( "Failed to validate message acknowledgement from '{:?}'", - client.username() + username.0 ); - client.kick(Text::translate( - MULTIPLAYER_DISCONNECT_CHAT_VALIDATION_FAILED, - [], - )); + commands.add(DisconnectClient { + client: acknowledgement.client, + reason: Text::translate(MULTIPLAYER_DISCONNECT_CHAT_VALIDATION_FAILED, []), + }); continue; } - debug!("Acknowledgement from '{:?}'", client.username()); + debug!("Acknowledgement from '{:?}'", username.0); } } fn handle_message_events( - mut clients: Query<&mut Client>, + mut clients: Query<(&Username, &mut Client)>, mut states: Query<&mut ChatState>, mut messages: EventReader, mut instances: Query<&mut Instance>, + mut commands: Commands, ) { let mut instance = instances.single_mut(); for message in messages.iter() { - let Ok(mut client) = clients.get_component_mut::(message.client) else { + let Ok((username, mut client)) = clients.get_mut(message.client) else { warn!("Unable to find client for message '{:?}'", message); continue; }; let Ok(mut state) = states.get_component_mut::(message.client) else { - warn!("Unable to find chat state for client '{:?}'", client.username()); + warn!("Unable to find chat state for client '{:?}'", username.0); continue; }; @@ -494,13 +500,13 @@ fn handle_message_events( if message.timestamp < state.last_message_timestamp { warn!( "{:?} sent out-of-order chat: '{:?}'", - client.username(), + username.0, message.message.as_ref() ); - client.kick(Text::translate( - MULTIPLAYER_DISCONNECT_OUT_OF_ORDER_CHAT, - [], - )); + commands.add(DisconnectClient { + client: message.client, + reason: Text::translate(MULTIPLAYER_DISCONNECT_OUT_OF_ORDER_CHAT, []), + }); continue; } @@ -512,11 +518,11 @@ fn handle_message_events( .validate(&message.acknowledgements, message.message_index) { None => { - warn!( - "Failed to validate acknowledgements from `{}`", - client.username().as_str() - ); - client.kick(MULTIPLAYER_DISCONNECT_CHAT_VALIDATION_FAILED); + warn!("Failed to validate acknowledgements from `{}`", username.0); + commands.add(DisconnectClient { + client: message.client, + reason: Text::translate(MULTIPLAYER_DISCONNECT_CHAT_VALIDATION_FAILED, []), + }); continue; } Some(mut last_seen) => { @@ -533,27 +539,28 @@ fn handle_message_events( }; let Some(session) = &state.session else { - client.kick(Text::translate( - CHAT_DISABLED_MISSING_PROFILE_KEY, - [], - )); + commands.add(DisconnectClient { + client: message.client, + reason: Text::translate(CHAT_DISABLED_MISSING_PROFILE_KEY, []) + }); continue; }; // Verify that the player's session has not expired if session.is_expired() { - client.send_message( - Text::translate(CHAT_DISABLED_EXPIRED_PROFILE_KEY, []).color(Color::RED), - ); + commands.add(DisconnectClient { + client: message.client, + reason: Text::translate(CHAT_DISABLED_EXPIRED_PROFILE_KEY, []), + }); continue; } // Verify that the chat message is signed let Some(message_signature) = &message.signature else { - client.kick(Text::translate( - MULTIPLAYER_DISCONNECT_UNSIGNED_CHAT, - [], - )); + commands.add(DisconnectClient { + client: message.client, + reason: Text::translate(MULTIPLAYER_DISCONNECT_UNSIGNED_CHAT, []) + }); continue; }; @@ -586,7 +593,10 @@ fn handle_message_events( ) .is_err() { - client.kick(Text::translate(MULTIPLAYER_DISCONNECT_UNSIGNED_CHAT, [])); + commands.add(DisconnectClient { + client: message.client, + reason: Text::translate(MULTIPLAYER_DISCONNECT_UNSIGNED_CHAT, []), + }); continue; } @@ -594,12 +604,12 @@ fn handle_message_events( let previous = last_seen .iter() .map(|sig| match state.signature_storage.index_of(sig) { - Some(index) => MessageSignature::ByIndex(*index), + Some(index) => MessageSignature::ByIndex(index), None => MessageSignature::BySignature(sig), }) .collect::>(); - info!("{}: {}", client.username(), message.message.as_ref()); + info!("{}: {}", username.0, message.message.as_ref()); instance.write_packet(&ChatMessageS2c { sender: link.sender, @@ -612,18 +622,22 @@ fn handle_message_events( unsigned_content: None, filter_type: MessageFilterType::PassThrough, chat_type: VarInt(0), - network_name: client.username().into_text().into(), + network_name: Text::from(username.0.clone()).into(), network_target_name: None, }); - for mut client in clients.iter_mut() { + // Update the other clients' chat states + for mut state in states.iter_mut() { // Add pending acknowledgement state.add_pending(&mut last_seen, message_signature.as_ref()); if state.validator.message_count() > 4096 { - client.kick(Text::translate( - MULTIPLAYER_DISCONNECT_TOO_MANY_PENDING_CHATS, - [], - )); + commands.add(DisconnectClient { + client: message.client, + reason: Text::translate( + MULTIPLAYER_DISCONNECT_TOO_MANY_PENDING_CHATS, + [], + ), + }); continue; } } diff --git a/crates/valence/src/server.rs b/crates/valence/src/server.rs index 3a802c712..eb1a57681 100644 --- a/crates/valence/src/server.rs +++ b/crates/valence/src/server.rs @@ -8,7 +8,6 @@ use anyhow::ensure; use bevy_app::prelude::*; use bevy_app::{ScheduleRunnerPlugin, ScheduleRunnerSettings}; use bevy_ecs::prelude::*; -use bevy_ecs::schedule::ScheduleLabel; use flume::{Receiver, Sender}; use rand::rngs::OsRng; use rsa::{PublicKeyParts, RsaPrivateKey}; @@ -18,23 +17,21 @@ use uuid::Uuid; use valence_nbt::{compound, Compound, List}; use valence_protocol::ident; use valence_protocol::types::Property; -use valence_protocol::username::Username; use crate::biome::{validate_biomes, Biome, BiomeId}; use crate::chat_type::{validate_chat_types, ChatType, ChatTypeId}; -use crate::client::event::{register_client_events, run_event_loop}; -use crate::client::{update_clients, Client}; +use crate::client::event::EventLoopSet; +use crate::client::{ClientBundle, ClientPlugin}; use crate::config::{AsyncCallbacks, ConnectionMode, ServerPlugin}; use crate::dimension::{validate_dimensions, Dimension, DimensionId}; -use crate::entity::{deinit_despawned_entities, init_entities, update_entities, McEntityManager}; -use crate::instance::{ - check_instance_invariants, update_instances_post_client, update_instances_pre_client, Instance, -}; -use crate::inventory::update_inventories; -use crate::player_list::{update_player_list, PlayerList}; -use crate::prelude::{Inventory, InventoryKind}; +use crate::entity::EntityPlugin; +use crate::instance::{Instance, InstancePlugin}; +use crate::inventory::InventoryPlugin; +use crate::player_list::PlayerListPlugin; +use crate::prelude::event::ClientEventPlugin; +use crate::prelude::ComponentPlugin; use crate::server::connect::do_accept_loop; -use crate::Despawned; +use crate::weather::WeatherPlugin; mod byte_channel; mod connect; @@ -94,9 +91,9 @@ struct SharedServerInner { /// Sent to all clients when joining. registry_codec: Compound, /// Sender for new clients past the login stage. - new_clients_send: Sender, + new_clients_send: Sender, /// Receiver for new clients past the login stage. - new_clients_recv: Receiver, + new_clients_recv: Receiver, /// A semaphore used to limit the number of simultaneous connections to the /// server. Closing this semaphore stops new connections. connection_sema: Arc, @@ -228,7 +225,7 @@ impl SharedServer { #[non_exhaustive] pub struct NewClientInfo { /// The username of the new client. - pub username: Username, + pub username: String, /// The UUID of the new client. pub uuid: Uuid, /// The remote address of the new client. @@ -327,24 +324,14 @@ pub fn build_plugin( break }; - world.spawn((client, Inventory::new(InventoryKind::Player))); + world.spawn(client); } }; let shared = server.shared.clone(); // Insert resources. - app.insert_resource(server) - .insert_resource(McEntityManager::new()) - .insert_resource(PlayerList::new()); - register_client_events(&mut app.world); - - // Add the event loop schedule. - let mut event_loop = Schedule::new(); - event_loop.configure_set(EventLoopSet); - event_loop.set_default_base_set(EventLoopSet); - - app.add_schedule(EventLoopSchedule, event_loop); + app.insert_resource(server); // Make the app loop forever at the configured TPS. { @@ -362,52 +349,42 @@ pub fn build_plugin( .in_base_set(StartupSet::PostStartup), ); - // Add `CoreSet:::PreUpdate` systems. - app.add_systems( - (spawn_new_clients.before(run_event_loop), run_event_loop).in_base_set(CoreSet::PreUpdate), + // Spawn new clients before the event loop starts. + app.add_system( + spawn_new_clients + .in_base_set(CoreSet::PreUpdate) + .before(EventLoopSet), ); - // Add internal valence systems that run after `CoreSet::Update`. - app.add_systems( - ( - init_entities, - check_instance_invariants, - update_player_list.before(update_instances_pre_client), - update_instances_pre_client.after(init_entities), - update_clients.after(update_instances_pre_client), - update_instances_post_client.after(update_clients), - deinit_despawned_entities.after(update_instances_post_client), - despawn_marked_entities.after(deinit_despawned_entities), - update_entities.after(despawn_marked_entities), + app.add_system(increment_tick_counter.in_base_set(CoreSet::Last)); + + // Add internal plugins. + app.add_plugin(ComponentPlugin) + .add_plugin(ClientPlugin) + .add_plugin(ClientEventPlugin) + .add_plugin(EntityPlugin) + .add_plugin(InstancePlugin) + .add_plugin(InventoryPlugin) + .add_plugin(PlayerListPlugin) + .add_plugin(WeatherPlugin); + + /* + println!( + "{}", + bevy_mod_debugdump::schedule_graph_dot( + app, + CoreSchedule::Main, + &bevy_mod_debugdump::schedule_graph::Settings { + ambiguity_enable: false, + ..Default::default() + }, ) - .in_base_set(CoreSet::PostUpdate), - ) - .add_systems( - update_inventories() - .in_base_set(CoreSet::PostUpdate) - .before(init_entities), - ) - .add_system(increment_tick_counter.in_base_set(CoreSet::Last)); + ); + */ Ok(()) } -/// The [`ScheduleLabel`] for the event loop [`Schedule`]. -#[derive(ScheduleLabel, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Debug)] -pub struct EventLoopSchedule; - -/// The default base set for the event loop [`Schedule`]. -#[derive(SystemSet, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Debug)] -pub struct EventLoopSet; - -/// Despawns all the entities marked as despawned with the [`Despawned`] -/// component. -fn despawn_marked_entities(mut commands: Commands, entities: Query>) { - for entity in &entities { - commands.entity(entity).despawn(); - } -} - fn increment_tick_counter(mut server: ResMut) { server.current_tick += 1; } From f5b59db31bc38000e46a6b66f818e849b1ef319a Mon Sep 17 00:00:00 2001 From: guac420 <60993440+guac42@users.noreply.github.com> Date: Fri, 24 Mar 2023 01:52:25 -0700 Subject: [PATCH 05/32] Formatting --- crates/valence/examples/chat.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/crates/valence/examples/chat.rs b/crates/valence/examples/chat.rs index d0a5183dc..3dd6b48f0 100644 --- a/crates/valence/examples/chat.rs +++ b/crates/valence/examples/chat.rs @@ -125,7 +125,8 @@ impl AcknowledgementValidator { /// Validate a set of `acknowledgements` offset by `message_index`. /// - /// Returns a [`VecDeque`] of acknowledged message signatures if the `acknowledgements` are valid and `None` if they are invalid. + /// Returns a [`VecDeque`] of acknowledged message signatures if the + /// `acknowledgements` are valid and `None` if they are invalid. pub fn validate( &mut self, acknowledgements: &[u8; 3], @@ -218,7 +219,8 @@ impl MessageSignatureStorage { self.indices.get(signature).copied() } - /// Update the signature storage according to `last_seen` while adding `signature` to the storage. + /// Update the signature storage according to `last_seen` while adding + /// `signature` to the storage. /// /// Warning: this consumes `last_seen`. pub fn add(&mut self, last_seen: &mut VecDeque<[u8; 256]>, signature: &[u8; 256]) { From a3eb7f7e4555716ba518ae54aab16b578e954f82 Mon Sep 17 00:00:00 2001 From: guac420 <60993440+guac42@users.noreply.github.com> Date: Fri, 24 Mar 2023 17:56:17 -0700 Subject: [PATCH 06/32] Move public key to assets --- .../yggdrasil_session_pubkey.der | Bin crates/valence/examples/chat.rs | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename {crates/valence/examples => assets}/yggdrasil_session_pubkey.der (100%) diff --git a/crates/valence/examples/yggdrasil_session_pubkey.der b/assets/yggdrasil_session_pubkey.der similarity index 100% rename from crates/valence/examples/yggdrasil_session_pubkey.der rename to assets/yggdrasil_session_pubkey.der diff --git a/crates/valence/examples/chat.rs b/crates/valence/examples/chat.rs index 3dd6b48f0..5323c5746 100644 --- a/crates/valence/examples/chat.rs +++ b/crates/valence/examples/chat.rs @@ -26,7 +26,7 @@ use valence::protocol::var_int::VarInt; const SPAWN_Y: i32 = 64; -const MOJANG_KEY_DATA: &[u8] = include_bytes!("./yggdrasil_session_pubkey.der"); +const MOJANG_KEY_DATA: &[u8] = include_bytes!("../../../assets/yggdrasil_session_pubkey.der"); #[derive(Resource)] struct MojangServicesState { From 051a18ab1533e325488c63b6a5d3bb371ce9a715 Mon Sep 17 00:00:00 2001 From: guac420 <60993440+guac42@users.noreply.github.com> Date: Wed, 29 Mar 2023 17:11:41 -0700 Subject: [PATCH 07/32] Create secure_chat plugin Extracts cryptographic chat support to an optional plugin and adds some documentation to what a chat_type is. --- crates/valence/examples/chat.rs | 626 +--------------------------- crates/valence/src/chat_type.rs | 3 +- crates/valence/src/lib.rs | 1 + crates/valence/src/secure_chat.rs | 656 ++++++++++++++++++++++++++++++ 4 files changed, 670 insertions(+), 616 deletions(-) create mode 100644 crates/valence/src/secure_chat.rs diff --git a/crates/valence/examples/chat.rs b/crates/valence/examples/chat.rs index 5323c5746..eb9f9539b 100644 --- a/crates/valence/examples/chat.rs +++ b/crates/valence/examples/chat.rs @@ -1,266 +1,12 @@ -use std::collections::{HashMap, HashSet, VecDeque}; -use std::time::SystemTime; - -use rsa::pkcs8::DecodePublicKey; -use rsa::{PaddingScheme, PublicKey, RsaPublicKey}; -use sha1::{Digest, Sha1}; -use sha2::Sha256; -use tracing::{debug, info, warn, Level}; -use valence::client::event::{ - ChatMessage, ClientSettings, CommandExecution, MessageAcknowledgment, PlayerSession, -}; +use tracing::{info, warn, Level}; +use valence::client::event::CommandExecution; use valence::client::{default_event_handler, despawn_disconnected_clients}; use valence::entity::player::PlayerBundle; use valence::prelude::*; -use valence::protocol::packet::c2s::play::client_settings::ChatMode; -use valence::protocol::packet::s2c::play::chat_message::{ChatMessageS2c, MessageFilterType}; -use valence::protocol::translation_key::{ - CHAT_DISABLED_CHAIN_BROKEN, CHAT_DISABLED_EXPIRED_PROFILE_KEY, - CHAT_DISABLED_MISSING_PROFILE_KEY, CHAT_DISABLED_OPTIONS, - MULTIPLAYER_DISCONNECT_CHAT_VALIDATION_FAILED, MULTIPLAYER_DISCONNECT_EXPIRED_PUBLIC_KEY, - MULTIPLAYER_DISCONNECT_INVALID_PUBLIC_KEY_SIGNATURE, MULTIPLAYER_DISCONNECT_OUT_OF_ORDER_CHAT, - MULTIPLAYER_DISCONNECT_TOO_MANY_PENDING_CHATS, MULTIPLAYER_DISCONNECT_UNSIGNED_CHAT, -}; -use valence::protocol::types::MessageSignature; -use valence::protocol::var_int::VarInt; +use valence::secure_chat::SecureChatPlugin; const SPAWN_Y: i32 = 64; -const MOJANG_KEY_DATA: &[u8] = include_bytes!("../../../assets/yggdrasil_session_pubkey.der"); - -#[derive(Resource)] -struct MojangServicesState { - public_key: RsaPublicKey, -} - -#[derive(Component, Debug)] -struct ChatState { - last_message_timestamp: u64, - chat_mode: ChatMode, - validator: AcknowledgementValidator, - chain: MessageChain, - signature_storage: MessageSignatureStorage, - session: Option, -} - -#[derive(Clone, Debug)] -struct AcknowledgementValidator { - messages: Vec>, - last_signature: Option<[u8; 256]>, -} - -#[derive(Clone, Debug)] -struct AcknowledgedMessage { - pub signature: [u8; 256], - pub pending: bool, -} - -#[derive(Clone, Debug)] -struct MessageChain { - link: Option, -} - -#[derive(Copy, Clone, Debug)] -struct MessageLink { - index: i32, - sender: Uuid, - session_id: Uuid, -} - -#[derive(Clone, Debug)] -struct MessageSignatureStorage { - signatures: [Option<[u8; 256]>; 128], - indices: HashMap<[u8; 256], i32>, -} - -#[derive(Clone, Debug)] -struct ChatSession { - expires_at: i64, - public_key: RsaPublicKey, -} - -impl ChatState { - pub fn add_pending(&mut self, last_seen: &mut VecDeque<[u8; 256]>, signature: &[u8; 256]) { - self.signature_storage.add(last_seen, signature); - self.validator.add_pending(signature); - } -} - -impl AcknowledgementValidator { - pub fn new() -> Self { - Self { - messages: vec![None; 20], - last_signature: None, - } - } - - /// Add a message pending acknowledgement via its `signature`. - pub fn add_pending(&mut self, signature: &[u8; 256]) { - if matches!(&self.last_signature, Some(last_sig) if signature == last_sig.as_ref()) { - return; - } - self.messages.push(Some(AcknowledgedMessage { - signature: *signature, - pending: true, - })); - self.last_signature = Some(*signature); - } - - /// Removes message signatures from the validator before an `index`. - /// - /// Message signatures will only be removed if the result leaves the - /// validator with at least 20 messages. Returns `true` if messages are - /// removed and `false` if they are not. - pub fn remove_until(&mut self, index: i32) -> bool { - // Ensure that there will still be 20 messages in the array - if index >= 0 && index <= (self.messages.len() - 20) as i32 { - self.messages.drain(0..index as usize); - if self.messages.len() < 20 { - warn!("Message validator 'messages' shrunk!"); - } - return true; - } - false - } - - /// Validate a set of `acknowledgements` offset by `message_index`. - /// - /// Returns a [`VecDeque`] of acknowledged message signatures if the - /// `acknowledgements` are valid and `None` if they are invalid. - pub fn validate( - &mut self, - acknowledgements: &[u8; 3], - message_index: i32, - ) -> Option> { - if !self.remove_until(message_index) { - // Invalid message index - return None; - } - - let acknowledged_count = { - let mut sum = 0u32; - for byte in acknowledgements { - sum += byte.count_ones(); - } - sum as usize - }; - - if acknowledged_count > 20 { - // Too many message acknowledgements, protocol error? - return None; - } - - let mut list = VecDeque::with_capacity(acknowledged_count); - for i in 0..20 { - let acknowledgement = acknowledgements[i >> 3] & (0b1 << (i % 8)) != 0; - // SAFETY: The length of messages is never less than 20 - let acknowledged_message = unsafe { self.messages.get_unchecked_mut(i) }; - // Client has acknowledged the i-th message - if acknowledgement { - // The validator has the i-th message - if let Some(m) = acknowledged_message { - m.pending = false; - list.push_back(m.signature); - } else { - // Client has acknowledged a non-existing message - warn!("Client has acknowledged a non-existing message"); - return None; - } - } else { - // Client has not acknowledged the i-th message - if matches!(acknowledged_message, Some(m) if !m.pending) { - // The validator has an i-th message that has been validated but the client - // claims that it hasn't been validated yet - warn!( - "The validator has an i-th message that has been validated but the client \ - claims that it hasn't been validated yet" - ); - return None; - } - // Honestly not entirely sure why this is done - if acknowledged_message.is_some() { - *acknowledged_message = None; - } - } - } - Some(list) - } - - /// The number of pending messages in the validator. - pub fn message_count(&self) -> usize { - self.messages.len() - } -} - -impl MessageChain { - pub fn next_link(&mut self) -> Option { - match &mut self.link { - None => self.link, - Some(current) => { - let temp = *current; - current.index += 1; - Some(temp) - } - } - } -} - -impl MessageLink { - pub fn update_hash(&self, hasher: &mut impl Digest) { - hasher.update(self.sender.into_bytes()); - hasher.update(self.session_id.into_bytes()); - hasher.update(self.index.to_be_bytes()); - } -} - -impl MessageSignatureStorage { - /// Get the index of the `signature` in the storage if it exists. - pub fn index_of(&self, signature: &[u8; 256]) -> Option { - self.indices.get(signature).copied() - } - - /// Update the signature storage according to `last_seen` while adding - /// `signature` to the storage. - /// - /// Warning: this consumes `last_seen`. - pub fn add(&mut self, last_seen: &mut VecDeque<[u8; 256]>, signature: &[u8; 256]) { - last_seen.push_back(*signature); - let mut sig_set = HashSet::new(); - for sig in last_seen.iter() { - sig_set.insert(*sig); - } - for i in 0..128 { - if last_seen.is_empty() { - return; - } - // Remove old message - let message_sig_data = self.signatures[i]; - // Add previously seen message - self.signatures[i] = last_seen.pop_back(); - if let Some(data) = self.signatures[i] { - self.indices.insert(data, i as i32); - } - // Reinsert old message if it is not already in last_seen - if let Some(data) = message_sig_data { - self.indices.remove(&data); - if sig_set.insert(data) { - last_seen.push_front(data); - } - } - } - } -} - -impl ChatSession { - pub fn is_expired(&self) -> bool { - SystemTime::now() - .duration_since(SystemTime::UNIX_EPOCH) - .expect("Unable to get Unix time") - .as_millis() - >= self.expires_at as u128 - } -} - pub fn main() { tracing_subscriber::fmt() .with_max_level(Level::DEBUG) @@ -268,32 +14,16 @@ pub fn main() { App::new() .add_plugin(ServerPlugin::new(())) + .add_plugin(SecureChatPlugin) .add_startup_system(setup) .add_system(init_clients) - .add_systems( - ( - default_event_handler, - handle_session_events, - handle_message_events, - handle_message_acknowledgement, - handle_command_events, - handle_chat_settings_event, - ) - .in_schedule(EventLoopSchedule), - ) + .add_systems((default_event_handler, handle_command_events).in_schedule(EventLoopSchedule)) .add_systems(PlayerList::default_systems()) .add_system(despawn_disconnected_clients) .run(); } fn setup(mut commands: Commands, server: Res) { - let mojang_pub_key = RsaPublicKey::from_public_key_der(MOJANG_KEY_DATA) - .expect("Error creating Mojang public key"); - - commands.insert_resource(MojangServicesState { - public_key: mojang_pub_key, - }); - let mut instance = server.new_instance(DimensionId::default()); for z in -5..5 { @@ -320,334 +50,17 @@ fn init_clients( *game_mode = GameMode::Adventure; client.send_message("Welcome to Valence! Talk about something.".italic()); - commands - .entity(entity) - .insert(PlayerBundle { - location: Location(instances.single()), - position: Position::new([0.0, SPAWN_Y as f64 + 1.0, 0.0]), - uuid: *uuid, - ..Default::default() - }) - .insert(ChatState { - last_message_timestamp: SystemTime::now() - .duration_since(SystemTime::UNIX_EPOCH) - .expect("Unable to get Unix time") - .as_millis() as u64, - chat_mode: ChatMode::Enabled, - validator: AcknowledgementValidator::new(), - chain: MessageChain { link: None }, - signature_storage: MessageSignatureStorage { - signatures: [None; 128], - indices: HashMap::new(), - }, - session: None, - }); + commands.entity(entity).insert(PlayerBundle { + location: Location(instances.single()), + position: Position::new([0.0, SPAWN_Y as f64 + 1.0, 0.0]), + uuid: *uuid, + ..Default::default() + }); info!("{} logged in!", username.0); } } -fn handle_session_events( - services_state: Res, - player_list: ResMut, - mut clients: Query<(&UniqueId, &Username, &mut ChatState)>, - mut sessions: EventReader, - mut commands: Commands, -) { - let pl = player_list.into_inner(); - - for session in sessions.iter() { - let Ok((uuid, username, mut state)) = clients.get_mut(session.client) else { - warn!("Unable to find client for session"); - continue; - }; - - let Some(player_entry) = pl.get_mut(uuid.0) else { - warn!("Unable to find '{}' in the player list", username.0); - continue; - }; - - // Verify that the session key has not expired - if SystemTime::now() - .duration_since(SystemTime::UNIX_EPOCH) - .expect("Unable to get Unix time") - .as_millis() - >= session.session_data.expires_at as u128 - { - warn!("Failed to validate profile key: expired public key"); - commands.add(DisconnectClient { - client: session.client, - reason: Text::translate(MULTIPLAYER_DISCONNECT_EXPIRED_PUBLIC_KEY, []), - }); - continue; - } - - // Serialize the session data - let mut serialized = Vec::with_capacity(318); - serialized.extend_from_slice(uuid.0.into_bytes().as_slice()); - serialized.extend_from_slice(session.session_data.expires_at.to_be_bytes().as_ref()); - serialized.extend_from_slice(session.session_data.public_key_data.as_ref()); - - // Hash the session data using the SHA-1 algorithm - let mut hasher = Sha1::new(); - hasher.update(&serialized); - let hash = hasher.finalize(); - - // Verify the session data using Mojang's public key and the hashed session data - // against the message signature - if services_state - .public_key - .verify( - PaddingScheme::new_pkcs1v15_sign::(), - &hash, - session.session_data.key_signature.as_ref(), - ) - .is_err() - { - warn!("Failed to validate profile key: invalid public key signature"); - commands.add(DisconnectClient { - client: session.client, - reason: Text::translate(MULTIPLAYER_DISCONNECT_INVALID_PUBLIC_KEY_SIGNATURE, []), - }); - } - - // Decode the player's session public key from the data - if let Ok(public_key) = - RsaPublicKey::from_public_key_der(session.session_data.public_key_data.as_ref()) - { - // Update the player's chat state data with the new player session data - state.chain.link = Some(MessageLink { - index: 0, - sender: uuid.0, - session_id: session.session_data.session_id, - }); - state.session = Some(ChatSession { - expires_at: session.session_data.expires_at, - public_key, - }); - } else { - // This shouldn't happen considering that it is highly unlikely that Mojang - // would provide the client with a malformed key. By this point the - // key signature has been verified - warn!("Received malformed profile key data from '{}'", username.0); - commands.add(DisconnectClient { - client: session.client, - reason: Text::translate( - MULTIPLAYER_DISCONNECT_EXPIRED_PUBLIC_KEY, - ["Malformed profile key data".color(Color::RED)], - ), - }); - } - - // Update the player list with the new session data - // The player list will then send this new session data to the other clients - player_entry.set_chat_data(Some(session.session_data.clone())); - } -} - -fn handle_message_acknowledgement( - mut clients: Query<(&Username, &mut ChatState)>, - mut acknowledgements: EventReader, - mut commands: Commands, -) { - for acknowledgement in acknowledgements.iter() { - let Ok((username, mut state)) = clients.get_mut(acknowledgement.client) else { - warn!("Unable to find client for acknowledgement"); - continue; - }; - - if !state.validator.remove_until(acknowledgement.message_index) { - warn!( - "Failed to validate message acknowledgement from '{:?}'", - username.0 - ); - commands.add(DisconnectClient { - client: acknowledgement.client, - reason: Text::translate(MULTIPLAYER_DISCONNECT_CHAT_VALIDATION_FAILED, []), - }); - continue; - } - - debug!("Acknowledgement from '{:?}'", username.0); - } -} - -fn handle_message_events( - mut clients: Query<(&Username, &mut Client)>, - mut states: Query<&mut ChatState>, - mut messages: EventReader, - mut instances: Query<&mut Instance>, - mut commands: Commands, -) { - let mut instance = instances.single_mut(); - - for message in messages.iter() { - let Ok((username, mut client)) = clients.get_mut(message.client) else { - warn!("Unable to find client for message '{:?}'", message); - continue; - }; - - let Ok(mut state) = states.get_component_mut::(message.client) else { - warn!("Unable to find chat state for client '{:?}'", username.0); - continue; - }; - - // Ensure that the client isn't sending messages while their chat is hidden - if state.chat_mode == ChatMode::Hidden { - client.send_message(Text::translate(CHAT_DISABLED_OPTIONS, []).color(Color::RED)); - continue; - } - - // Ensure we are receiving chat messages in order - if message.timestamp < state.last_message_timestamp { - warn!( - "{:?} sent out-of-order chat: '{:?}'", - username.0, - message.message.as_ref() - ); - commands.add(DisconnectClient { - client: message.client, - reason: Text::translate(MULTIPLAYER_DISCONNECT_OUT_OF_ORDER_CHAT, []), - }); - continue; - } - - state.last_message_timestamp = message.timestamp; - - // Validate the message acknowledgements - match state - .validator - .validate(&message.acknowledgements, message.message_index) - { - None => { - warn!("Failed to validate acknowledgements from `{}`", username.0); - commands.add(DisconnectClient { - client: message.client, - reason: Text::translate(MULTIPLAYER_DISCONNECT_CHAT_VALIDATION_FAILED, []), - }); - continue; - } - Some(mut last_seen) => { - // This whole process should probably be done on another thread similarly to - // chunk loading in the 'terrain.rs' example, as this is what - // the notchian server does - - let Some(link) = &state.chain.next_link() else { - client.send_message(Text::translate( - CHAT_DISABLED_CHAIN_BROKEN, - [], - ).color(Color::RED)); - continue; - }; - - let Some(session) = &state.session else { - commands.add(DisconnectClient { - client: message.client, - reason: Text::translate(CHAT_DISABLED_MISSING_PROFILE_KEY, []) - }); - continue; - }; - - // Verify that the player's session has not expired - if session.is_expired() { - commands.add(DisconnectClient { - client: message.client, - reason: Text::translate(CHAT_DISABLED_EXPIRED_PROFILE_KEY, []), - }); - continue; - } - - // Verify that the chat message is signed - let Some(message_signature) = &message.signature else { - commands.add(DisconnectClient { - client: message.client, - reason: Text::translate(MULTIPLAYER_DISCONNECT_UNSIGNED_CHAT, []) - }); - continue; - }; - - // Create the hash digest used to verify the chat message - let mut hasher = Sha256::new_with_prefix([0u8, 0, 0, 1]); - - // Update the hash with the player's message chain state - link.update_hash(&mut hasher); - - // Update the hash with the message contents - hasher.update(message.salt.to_be_bytes()); - hasher.update((message.timestamp / 1000).to_be_bytes()); - let bytes = message.message.as_bytes(); - hasher.update((bytes.len() as u32).to_be_bytes()); - hasher.update(bytes); - hasher.update((last_seen.len() as u32).to_be_bytes()); - for sig in last_seen.iter() { - hasher.update(sig); - } - let hashed = hasher.finalize(); - - // Verify the chat message using the player's session public key and hashed data - // against the message signature - if session - .public_key - .verify( - PaddingScheme::new_pkcs1v15_sign::(), - &hashed, - message_signature.as_ref(), - ) - .is_err() - { - commands.add(DisconnectClient { - client: message.client, - reason: Text::translate(MULTIPLAYER_DISCONNECT_UNSIGNED_CHAT, []), - }); - continue; - } - - // Create a list of messages that have been seen by the client - let previous = last_seen - .iter() - .map(|sig| match state.signature_storage.index_of(sig) { - Some(index) => MessageSignature::ByIndex(index), - None => MessageSignature::BySignature(sig), - }) - .collect::>(); - - info!("{}: {}", username.0, message.message.as_ref()); - - instance.write_packet(&ChatMessageS2c { - sender: link.sender, - index: VarInt(link.index), - message_signature: Some(message_signature.as_ref()), - message: message.message.as_ref(), - time_stamp: message.timestamp, - salt: message.salt, - previous_messages: previous, - unsigned_content: None, - filter_type: MessageFilterType::PassThrough, - chat_type: VarInt(0), - network_name: Text::from(username.0.clone()).into(), - network_target_name: None, - }); - - // Update the other clients' chat states - for mut state in states.iter_mut() { - // Add pending acknowledgement - state.add_pending(&mut last_seen, message_signature.as_ref()); - if state.validator.message_count() > 4096 { - commands.add(DisconnectClient { - client: message.client, - reason: Text::translate( - MULTIPLAYER_DISCONNECT_TOO_MANY_PENDING_CHATS, - [], - ), - }); - continue; - } - } - } - } - } -} - fn handle_command_events( mut clients: Query<&mut Client>, mut commands: EventReader, @@ -666,20 +79,3 @@ fn handle_command_events( client.send_message(formatted); } } - -fn handle_chat_settings_event( - mut states: Query<&mut ChatState>, - mut settings: EventReader, -) { - for ClientSettings { - client, chat_mode, .. - } in settings.iter() - { - let Ok(mut state) = states.get_component_mut::(*client) else { - warn!("Unable to find chat state for client"); - continue; - }; - - state.chat_mode = *chat_mode; - } -} diff --git a/crates/valence/src/chat_type.rs b/crates/valence/src/chat_type.rs index f447c01bf..5da83a92b 100644 --- a/crates/valence/src/chat_type.rs +++ b/crates/valence/src/chat_type.rs @@ -20,7 +20,8 @@ use valence_protocol::text::Color; #[derive(Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)] pub struct ChatTypeId(pub(crate) u16); -/// Contains the configuration for a chat type. +/// Contains information about how chat is styled, such as the chat color. The +/// notchian server has different chat types for team chat and direct messages. /// /// Chat types are registered once at startup through /// [`ServerPlugin::with_chat_types`] diff --git a/crates/valence/src/lib.rs b/crates/valence/src/lib.rs index 359773c84..f01a3dc03 100644 --- a/crates/valence/src/lib.rs +++ b/crates/valence/src/lib.rs @@ -38,6 +38,7 @@ pub mod inventory; pub mod packet; pub mod player_list; pub mod player_textures; +pub mod secure_chat; pub mod server; #[cfg(any(test, doctest))] mod unit_test; diff --git a/crates/valence/src/secure_chat.rs b/crates/valence/src/secure_chat.rs new file mode 100644 index 000000000..c25bbd6ad --- /dev/null +++ b/crates/valence/src/secure_chat.rs @@ -0,0 +1,656 @@ +use std::collections::{HashMap, HashSet, VecDeque}; +use std::time::SystemTime; + +use bevy_app::prelude::*; +use bevy_ecs::prelude::*; +use rsa::pkcs8::DecodePublicKey; +use rsa::{PaddingScheme, PublicKey, RsaPublicKey}; +use sha1::{Digest, Sha1}; +use sha2::Sha256; +use tracing::{debug, info, warn}; +use uuid::Uuid; +use valence_protocol::packet::c2s::play::client_settings::ChatMode; +use valence_protocol::packet::s2c::play::chat_message::MessageFilterType; +use valence_protocol::packet::s2c::play::ChatMessageS2c; +use valence_protocol::text::{Color, Text, TextFormat}; +use valence_protocol::translation_key::{ + CHAT_DISABLED_CHAIN_BROKEN, CHAT_DISABLED_EXPIRED_PROFILE_KEY, + CHAT_DISABLED_MISSING_PROFILE_KEY, CHAT_DISABLED_OPTIONS, + MULTIPLAYER_DISCONNECT_CHAT_VALIDATION_FAILED, MULTIPLAYER_DISCONNECT_EXPIRED_PUBLIC_KEY, + MULTIPLAYER_DISCONNECT_INVALID_PUBLIC_KEY_SIGNATURE, MULTIPLAYER_DISCONNECT_OUT_OF_ORDER_CHAT, + MULTIPLAYER_DISCONNECT_TOO_MANY_PENDING_CHATS, MULTIPLAYER_DISCONNECT_UNSIGNED_CHAT, +}; +use valence_protocol::types::MessageSignature; +use valence_protocol::var_int::VarInt; + +use crate::client::event::{ChatMessage, ClientSettings, MessageAcknowledgment, PlayerSession}; +use crate::client::{Client, DisconnectClient, FlushPacketsSet}; +use crate::component::{UniqueId, Username}; +use crate::instance::Instance; +use crate::player_list::PlayerList; + +const MOJANG_KEY_DATA: &[u8] = include_bytes!("../../../assets/yggdrasil_session_pubkey.der"); + +#[derive(Resource)] +struct MojangServicesState { + public_key: RsaPublicKey, +} + +impl MojangServicesState { + pub fn new(public_key: RsaPublicKey) -> Self { + Self { public_key } + } +} + +#[derive(Debug, Component)] +struct ChatState { + last_message_timestamp: u64, + chat_mode: ChatMode, + validator: AcknowledgementValidator, + chain: MessageChain, + signature_storage: MessageSignatureStorage, + session: Option, +} + +impl Default for ChatState { + fn default() -> Self { + Self { + last_message_timestamp: SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .expect("Unable to get Unix time") + .as_millis() as u64, + chat_mode: ChatMode::Enabled, + validator: AcknowledgementValidator::new(), + chain: MessageChain::new(), + signature_storage: MessageSignatureStorage::new(), + session: None, + } + } +} + +impl ChatState { + pub fn add_pending(&mut self, last_seen: &mut VecDeque<[u8; 256]>, signature: &[u8; 256]) { + self.signature_storage.add(last_seen, signature); + self.validator.add_pending(signature); + } +} + +#[derive(Clone, Debug)] +struct AcknowledgementValidator { + messages: Vec>, + last_signature: Option<[u8; 256]>, +} + +impl AcknowledgementValidator { + pub fn new() -> Self { + Self { + messages: vec![None; 20], + last_signature: None, + } + } + + /// Add a message pending acknowledgement via its `signature`. + pub fn add_pending(&mut self, signature: &[u8; 256]) { + if matches!(&self.last_signature, Some(last_sig) if signature == last_sig.as_ref()) { + return; + } + self.messages.push(Some(AcknowledgedMessage { + signature: *signature, + pending: true, + })); + self.last_signature = Some(*signature); + } + + /// Removes message signatures from the validator before an `index`. + /// + /// Message signatures will only be removed if the result leaves the + /// validator with at least 20 messages. Returns `true` if messages are + /// removed and `false` if they are not. + pub fn remove_until(&mut self, index: i32) -> bool { + // Ensure that there will still be 20 messages in the array + if index >= 0 && index <= (self.messages.len() - 20) as i32 { + self.messages.drain(0..index as usize); + if self.messages.len() < 20 { + warn!("Message validator 'messages' shrunk!"); + } + return true; + } + false + } + + /// Validate a set of `acknowledgements` offset by `message_index`. + /// + /// Returns a [`VecDeque`] of acknowledged message signatures if the + /// `acknowledgements` are valid and `None` if they are invalid. + pub fn validate( + &mut self, + acknowledgements: &[u8; 3], + message_index: i32, + ) -> Option> { + if !self.remove_until(message_index) { + // Invalid message index + return None; + } + + let acknowledged_count = { + let mut sum = 0u32; + for byte in acknowledgements { + sum += byte.count_ones(); + } + sum as usize + }; + + if acknowledged_count > 20 { + // Too many message acknowledgements, protocol error? + return None; + } + + let mut list = VecDeque::with_capacity(acknowledged_count); + for i in 0..20 { + let acknowledgement = acknowledgements[i >> 3] & (0b1 << (i % 8)) != 0; + // SAFETY: The length of messages is never less than 20 + let acknowledged_message = unsafe { self.messages.get_unchecked_mut(i) }; + // Client has acknowledged the i-th message + if acknowledgement { + // The validator has the i-th message + if let Some(m) = acknowledged_message { + m.pending = false; + list.push_back(m.signature); + } else { + // Client has acknowledged a non-existing message + warn!("Client has acknowledged a non-existing message"); + return None; + } + } else { + // Client has not acknowledged the i-th message + if matches!(acknowledged_message, Some(m) if !m.pending) { + // The validator has an i-th message that has been validated but the client + // claims that it hasn't been validated yet + warn!( + "The validator has an i-th message that has been validated but the client \ + claims that it hasn't been validated yet" + ); + return None; + } + // Honestly not entirely sure why this is done + if acknowledged_message.is_some() { + *acknowledged_message = None; + } + } + } + Some(list) + } + + /// The number of pending messages in the validator. + pub fn message_count(&self) -> usize { + self.messages.len() + } +} + +#[derive(Clone, Debug)] +struct AcknowledgedMessage { + pub signature: [u8; 256], + pub pending: bool, +} + +#[derive(Clone, Default, Debug)] +struct MessageChain { + link: Option, +} + +impl MessageChain { + pub fn new() -> Self { + Self::default() + } + + pub fn next_link(&mut self) -> Option { + match &mut self.link { + None => self.link, + Some(current) => { + let temp = *current; + current.index += 1; + Some(temp) + } + } + } +} + +#[derive(Copy, Clone, Debug)] +struct MessageLink { + index: i32, + sender: Uuid, + session_id: Uuid, +} + +impl MessageLink { + pub fn update_hash(&self, hasher: &mut impl Digest) { + hasher.update(self.sender.into_bytes()); + hasher.update(self.session_id.into_bytes()); + hasher.update(self.index.to_be_bytes()); + } +} + +#[derive(Clone, Debug)] +struct MessageSignatureStorage { + signatures: [Option<[u8; 256]>; 128], + indices: HashMap<[u8; 256], i32>, +} + +impl Default for MessageSignatureStorage { + fn default() -> Self { + Self { + signatures: [None; 128], + indices: HashMap::new(), + } + } +} + +impl MessageSignatureStorage { + pub fn new() -> Self { + Self::default() + } + + /// Get the index of the `signature` in the storage if it exists. + pub fn index_of(&self, signature: &[u8; 256]) -> Option { + self.indices.get(signature).copied() + } + + /// Update the signature storage according to `last_seen` while adding + /// `signature` to the storage. + /// + /// Warning: this consumes `last_seen`. + pub fn add(&mut self, last_seen: &mut VecDeque<[u8; 256]>, signature: &[u8; 256]) { + last_seen.push_back(*signature); + let mut sig_set = HashSet::new(); + for sig in last_seen.iter() { + sig_set.insert(*sig); + } + for i in 0..128 { + if last_seen.is_empty() { + return; + } + // Remove old message + let message_sig_data = self.signatures[i]; + // Add previously seen message + self.signatures[i] = last_seen.pop_back(); + if let Some(data) = self.signatures[i] { + self.indices.insert(data, i as i32); + } + // Reinsert old message if it is not already in last_seen + if let Some(data) = message_sig_data { + self.indices.remove(&data); + if sig_set.insert(data) { + last_seen.push_front(data); + } + } + } + } +} + +#[derive(Clone, Debug)] +struct ChatSession { + expires_at: i64, + public_key: RsaPublicKey, +} + +impl ChatSession { + pub fn is_expired(&self) -> bool { + SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .expect("Unable to get Unix time") + .as_millis() + >= self.expires_at as u128 + } +} + +pub struct SecureChatPlugin; + +impl Plugin for SecureChatPlugin { + fn build(&self, app: &mut bevy_app::App) { + let mojang_pub_key = RsaPublicKey::from_public_key_der(MOJANG_KEY_DATA) + .expect("Error creating Mojang public key"); + + app.insert_resource(MojangServicesState::new(mojang_pub_key)) + .add_systems( + ( + init_chat_states, + handle_session_events + .after(init_chat_states) + .before(handle_message_events), + handle_message_acknowledgement + .after(init_chat_states) + .before(handle_message_events), + handle_chat_settings_event + .after(init_chat_states) + .before(handle_message_events), + handle_message_events.after(init_chat_states), + ) + .in_base_set(CoreSet::PostUpdate) + .before(FlushPacketsSet), + ); + } +} + +fn init_chat_states(clients: Query>, mut commands: Commands) { + for entity in clients.iter() { + commands.entity(entity).insert(ChatState::default()); + } +} + +fn handle_session_events( + services_state: Res, + player_list: ResMut, + mut clients: Query<(&UniqueId, &Username, &mut ChatState)>, + mut sessions: EventReader, + mut commands: Commands, +) { + let pl = player_list.into_inner(); + + for session in sessions.iter() { + let Ok((uuid, username, mut state)) = clients.get_mut(session.client) else { + warn!("Unable to find client for session"); + continue; + }; + + let Some(player_entry) = pl.get_mut(uuid.0) else { + warn!("Unable to find '{}' in the player list", username.0); + continue; + }; + + // Verify that the session key has not expired + if SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .expect("Unable to get Unix time") + .as_millis() + >= session.session_data.expires_at as u128 + { + warn!("Failed to validate profile key: expired public key"); + commands.add(DisconnectClient { + client: session.client, + reason: Text::translate(MULTIPLAYER_DISCONNECT_EXPIRED_PUBLIC_KEY, []), + }); + continue; + } + + // Serialize the session data + let mut serialized = Vec::with_capacity(318); + serialized.extend_from_slice(uuid.0.into_bytes().as_slice()); + serialized.extend_from_slice(session.session_data.expires_at.to_be_bytes().as_ref()); + serialized.extend_from_slice(session.session_data.public_key_data.as_ref()); + + // Hash the session data using the SHA-1 algorithm + let mut hasher = Sha1::new(); + hasher.update(&serialized); + let hash = hasher.finalize(); + + // Verify the session data using Mojang's public key and the hashed session data + // against the message signature + if services_state + .public_key + .verify( + PaddingScheme::new_pkcs1v15_sign::(), + &hash, + session.session_data.key_signature.as_ref(), + ) + .is_err() + { + warn!("Failed to validate profile key: invalid public key signature"); + commands.add(DisconnectClient { + client: session.client, + reason: Text::translate(MULTIPLAYER_DISCONNECT_INVALID_PUBLIC_KEY_SIGNATURE, []), + }); + } + + // Decode the player's session public key from the data + if let Ok(public_key) = + RsaPublicKey::from_public_key_der(session.session_data.public_key_data.as_ref()) + { + // Update the player's chat state data with the new player session data + state.chain.link = Some(MessageLink { + index: 0, + sender: uuid.0, + session_id: session.session_data.session_id, + }); + state.session = Some(ChatSession { + expires_at: session.session_data.expires_at, + public_key, + }); + } else { + // This shouldn't happen considering that it is highly unlikely that Mojang + // would provide the client with a malformed key. By this point the + // key signature has been verified + warn!("Received malformed profile key data from '{}'", username.0); + commands.add(DisconnectClient { + client: session.client, + reason: Text::translate( + MULTIPLAYER_DISCONNECT_EXPIRED_PUBLIC_KEY, + ["Malformed profile key data".color(Color::RED)], + ), + }); + } + + // Update the player list with the new session data + // The player list will then send this new session data to the other clients + player_entry.set_chat_data(Some(session.session_data.clone())); + } +} + +fn handle_message_acknowledgement( + mut clients: Query<(&Username, &mut ChatState)>, + mut acknowledgements: EventReader, + mut commands: Commands, +) { + for acknowledgement in acknowledgements.iter() { + let Ok((username, mut state)) = clients.get_mut(acknowledgement.client) else { + warn!("Unable to find client for acknowledgement"); + continue; + }; + + if !state.validator.remove_until(acknowledgement.message_index) { + warn!( + "Failed to validate message acknowledgement from '{:?}'", + username.0 + ); + commands.add(DisconnectClient { + client: acknowledgement.client, + reason: Text::translate(MULTIPLAYER_DISCONNECT_CHAT_VALIDATION_FAILED, []), + }); + continue; + } + + debug!("Acknowledgement from '{:?}'", username.0); + } +} + +fn handle_message_events( + mut clients: Query<(&Username, &mut Client)>, + mut states: Query<&mut ChatState>, + mut messages: EventReader, + mut instances: Query<&mut Instance>, + mut commands: Commands, +) { + let mut instance = instances.single_mut(); + + for message in messages.iter() { + let Ok((username, mut client)) = clients.get_mut(message.client) else { + warn!("Unable to find client for message '{:?}'", message); + continue; + }; + + let Ok(mut state) = states.get_component_mut::(message.client) else { + warn!("Unable to find chat state for client '{:?}'", username.0); + continue; + }; + + // Ensure that the client isn't sending messages while their chat is hidden + if state.chat_mode == ChatMode::Hidden { + client.send_message(Text::translate(CHAT_DISABLED_OPTIONS, []).color(Color::RED)); + continue; + } + + // Ensure we are receiving chat messages in order + if message.timestamp < state.last_message_timestamp { + warn!( + "{:?} sent out-of-order chat: '{:?}'", + username.0, + message.message.as_ref() + ); + commands.add(DisconnectClient { + client: message.client, + reason: Text::translate(MULTIPLAYER_DISCONNECT_OUT_OF_ORDER_CHAT, []), + }); + continue; + } + + state.last_message_timestamp = message.timestamp; + + // Validate the message acknowledgements + match state + .validator + .validate(&message.acknowledgements, message.message_index) + { + None => { + warn!("Failed to validate acknowledgements from `{}`", username.0); + commands.add(DisconnectClient { + client: message.client, + reason: Text::translate(MULTIPLAYER_DISCONNECT_CHAT_VALIDATION_FAILED, []), + }); + continue; + } + Some(mut last_seen) => { + // This whole process should probably be done on another thread similarly to + // chunk loading in the 'terrain.rs' example, as this is what + // the notchian server does + + let Some(link) = &state.chain.next_link() else { + client.send_message(Text::translate( + CHAT_DISABLED_CHAIN_BROKEN, + [], + ).color(Color::RED)); + continue; + }; + + let Some(session) = &state.session else { + commands.add(DisconnectClient { + client: message.client, + reason: Text::translate(CHAT_DISABLED_MISSING_PROFILE_KEY, []) + }); + continue; + }; + + // Verify that the player's session has not expired + if session.is_expired() { + commands.add(DisconnectClient { + client: message.client, + reason: Text::translate(CHAT_DISABLED_EXPIRED_PROFILE_KEY, []), + }); + continue; + } + + // Verify that the chat message is signed + let Some(message_signature) = &message.signature else { + commands.add(DisconnectClient { + client: message.client, + reason: Text::translate(MULTIPLAYER_DISCONNECT_UNSIGNED_CHAT, []) + }); + continue; + }; + + // Create the hash digest used to verify the chat message + let mut hasher = Sha256::new_with_prefix([0u8, 0, 0, 1]); + + // Update the hash with the player's message chain state + link.update_hash(&mut hasher); + + // Update the hash with the message contents + hasher.update(message.salt.to_be_bytes()); + hasher.update((message.timestamp / 1000).to_be_bytes()); + let bytes = message.message.as_bytes(); + hasher.update((bytes.len() as u32).to_be_bytes()); + hasher.update(bytes); + hasher.update((last_seen.len() as u32).to_be_bytes()); + for sig in last_seen.iter() { + hasher.update(sig); + } + let hashed = hasher.finalize(); + + // Verify the chat message using the player's session public key and hashed data + // against the message signature + if session + .public_key + .verify( + PaddingScheme::new_pkcs1v15_sign::(), + &hashed, + message_signature.as_ref(), + ) + .is_err() + { + commands.add(DisconnectClient { + client: message.client, + reason: Text::translate(MULTIPLAYER_DISCONNECT_UNSIGNED_CHAT, []), + }); + continue; + } + + // Create a list of messages that have been seen by the client + let previous = last_seen + .iter() + .map(|sig| match state.signature_storage.index_of(sig) { + Some(index) => MessageSignature::ByIndex(index), + None => MessageSignature::BySignature(sig), + }) + .collect::>(); + + info!("{}: {}", username.0, message.message.as_ref()); + + instance.write_packet(&ChatMessageS2c { + sender: link.sender, + index: VarInt(link.index), + message_signature: Some(message_signature.as_ref()), + message: message.message.as_ref(), + time_stamp: message.timestamp, + salt: message.salt, + previous_messages: previous, + unsigned_content: None, + filter_type: MessageFilterType::PassThrough, + chat_type: VarInt(0), + network_name: Text::from(username.0.clone()).into(), + network_target_name: None, + }); + + // Update the other clients' chat states + for mut state in states.iter_mut() { + // Add pending acknowledgement + state.add_pending(&mut last_seen, message_signature.as_ref()); + if state.validator.message_count() > 4096 { + commands.add(DisconnectClient { + client: message.client, + reason: Text::translate( + MULTIPLAYER_DISCONNECT_TOO_MANY_PENDING_CHATS, + [], + ), + }); + continue; + } + } + } + } + } +} + +fn handle_chat_settings_event( + mut states: Query<&mut ChatState>, + mut settings: EventReader, +) { + for ClientSettings { + client, chat_mode, .. + } in settings.iter() + { + let Ok(mut state) = states.get_component_mut::(*client) else { + warn!("Unable to find chat state for client"); + continue; + }; + + state.chat_mode = *chat_mode; + } +} From 649842b7ed6806f2ea00f46d9c7a9267ebda15b8 Mon Sep 17 00:00:00 2001 From: guac420 <60993440+guac42@users.noreply.github.com> Date: Thu, 6 Apr 2023 18:42:55 -0700 Subject: [PATCH 08/32] Fix merge conflicts --- crates/valence/examples/chat.rs | 39 +- crates/valence/src/chat_type.rs | 376 ++++++++++++------ crates/valence/src/client/event.rs | 116 ++++-- crates/valence/src/config.rs | 70 ---- crates/valence/src/lib.rs | 10 +- crates/valence/src/secure_chat.rs | 4 + crates/valence/src/server.rs | 163 +------- .../src/packet/s2c/play/chat_message.rs | 41 +- 8 files changed, 397 insertions(+), 422 deletions(-) diff --git a/crates/valence/examples/chat.rs b/crates/valence/examples/chat.rs index eb9f9539b..54befbd6e 100644 --- a/crates/valence/examples/chat.rs +++ b/crates/valence/examples/chat.rs @@ -1,7 +1,9 @@ -use tracing::{info, warn, Level}; +#![allow(clippy::type_complexity)] + +use tracing::{warn, Level}; use valence::client::event::CommandExecution; use valence::client::{default_event_handler, despawn_disconnected_clients}; -use valence::entity::player::PlayerBundle; +use valence::entity::player::PlayerEntityBundle; use valence::prelude::*; use valence::secure_chat::SecureChatPlugin; @@ -17,14 +19,25 @@ pub fn main() { .add_plugin(SecureChatPlugin) .add_startup_system(setup) .add_system(init_clients) - .add_systems((default_event_handler, handle_command_events).in_schedule(EventLoopSchedule)) - .add_systems(PlayerList::default_systems()) .add_system(despawn_disconnected_clients) + .add_systems( + ( + default_event_handler, + handle_command_events + ) + .in_schedule(EventLoopSchedule), + ) + .add_systems(PlayerList::default_systems()) .run(); } -fn setup(mut commands: Commands, server: Res) { - let mut instance = server.new_instance(DimensionId::default()); +fn setup( + mut commands: Commands, + server: Res, + dimensions: Query<&DimensionType>, + biomes: Query<&Biome>, +) { + let mut instance = Instance::new(ident!("overworld"), &dimensions, &biomes, &server); for z in -5..5 { for x in -5..5 { @@ -34,7 +47,7 @@ fn setup(mut commands: Commands, server: Res) { for z in -25..25 { for x in -25..25 { - instance.set_block([x, SPAWN_Y, z], BlockState::BEDROCK); + instance.set_block([x, SPAWN_Y, z], BlockState::GRASS_BLOCK); } } @@ -42,22 +55,20 @@ fn setup(mut commands: Commands, server: Res) { } fn init_clients( - mut clients: Query<(Entity, &UniqueId, &Username, &mut Client, &mut GameMode), Added>, + mut clients: Query<(Entity, &UniqueId, &mut Client, &mut GameMode), Added>, instances: Query>, mut commands: Commands, ) { - for (entity, uuid, username, mut client, mut game_mode) in &mut clients { - *game_mode = GameMode::Adventure; - client.send_message("Welcome to Valence! Talk about something.".italic()); + for (entity, uuid, mut client, mut game_mode) in &mut clients { + *game_mode = GameMode::Creative; + client.send_message("Welcome to Valence! Say something.".italic()); - commands.entity(entity).insert(PlayerBundle { + commands.entity(entity).insert(PlayerEntityBundle { location: Location(instances.single()), position: Position::new([0.0, SPAWN_Y as f64 + 1.0, 0.0]), uuid: *uuid, ..Default::default() }); - - info!("{} logged in!", username.0); } } diff --git a/crates/valence/src/chat_type.rs b/crates/valence/src/chat_type.rs index 5da83a92b..54213efee 100644 --- a/crates/valence/src/chat_type.rs +++ b/crates/valence/src/chat_type.rs @@ -1,144 +1,74 @@ //! ChatType configuration and identification. +//! +//! **NOTE:** +//! +//! - Modifying the chat type registry after the server has started can +//! break invariants within instances and clients! Make sure there are no +//! instances or clients spawned before mutating. -use std::collections::HashSet; +use std::ops::Index; -use anyhow::ensure; -use valence_nbt::{compound, Compound, List}; +use anyhow::{bail, Context}; +use bevy_app::{CoreSet, Plugin, StartupSet}; +use bevy_ecs::prelude::*; +use tracing::error; +use valence_nbt::{compound, Compound, List, Value}; use valence_protocol::ident; use valence_protocol::ident::Ident; use valence_protocol::text::Color; -/// Identifies a particular [`ChatType`] on the server. -/// -/// The default chat type ID refers to the first chat type added in -/// [`ServerPlugin::chat_types`]. -/// -/// To obtain chat type IDs for other chat types, see -/// [`ServerPlugin::chat_types`]. -/// -/// [`ServerPlugin::chat_types`]: crate::config::ServerPlugin::chat_types -#[derive(Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)] -pub struct ChatTypeId(pub(crate) u16); +use crate::registry_codec::{RegistryCodec, RegistryCodecSet, RegistryValue}; + +#[derive(Resource)] +pub struct ChatTypeRegistry { + id_to_chat_type: Vec, +} + +impl ChatTypeRegistry { + pub const KEY: Ident<&str> = ident!("minecraft:chat_type"); + + pub fn get_by_id(&self, id: ChatTypeId) -> Option { + self.id_to_chat_type.get(id.0 as usize).cloned() + } + + pub fn iter(&self) -> impl Iterator + '_ { + self.id_to_chat_type + .iter() + .enumerate() + .map(|(id, chat_type)| (ChatTypeId(id as _), *chat_type)) + } +} + +impl Index for ChatTypeRegistry { + type Output = Entity; + + fn index(&self, index: ChatTypeId) -> &Self::Output { + self.id_to_chat_type + .get(index.0 as usize) + .unwrap_or_else(|| panic!("invalid {index:?}")) + } +} + +/// An index into the chat type registry +#[derive(Component, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Default)] +pub struct ChatTypeId(pub u16); /// Contains information about how chat is styled, such as the chat color. The /// notchian server has different chat types for team chat and direct messages. /// -/// Chat types are registered once at startup through -/// [`ServerPlugin::with_chat_types`] -/// /// Note that [`ChatTypeDecoration::style`] for [`ChatType::narration`] /// is unused by the notchian client and is ignored. -/// -/// [`ServerPlugin::with_chat_types`]: crate::config::ServerPlugin::with_chat_types -#[derive(Clone, Debug)] +#[derive(Component, Clone, Debug)] pub struct ChatType { pub name: Ident, pub chat: ChatTypeDecoration, pub narration: ChatTypeDecoration, } -impl ChatType { - pub(crate) fn to_chat_type_registry_item(&self, id: i32) -> Compound { - compound! { - "name" => self.name.clone(), - "id" => id, - "element" => compound! { - "chat" => { - let mut chat = compound! { - "translation_key" => self.chat.translation_key.clone(), - "parameters" => { - let mut parameters = Vec::new(); - if self.chat.parameters.sender { - parameters.push("sender".to_string()); - } - if self.chat.parameters.target { - parameters.push("target".to_string()); - } - if self.chat.parameters.content { - parameters.push("content".to_string()); - } - List::String(parameters) - }, - }; - if let Some(style) = &self.chat.style { - let mut s = Compound::new(); - if let Some(color) = style.color { - s.insert("color", format!("#{:02x}{:02x}{:02x}", color.r, color.g, color.b)); - } - if let Some(bold) = style.bold { - s.insert("bold", bold); - } - if let Some(italic) = style.italic { - s.insert("italic", italic); - } - if let Some(underlined) = style.underlined { - s.insert("underlined", underlined); - } - if let Some(strikethrough) = style.strikethrough { - s.insert("strikethrough", strikethrough); - } - if let Some(obfuscated) = style.obfuscated { - s.insert("obfuscated", obfuscated); - } - if let Some(insertion) = &style.insertion { - s.insert("insertion", insertion.clone()); - } - if let Some(font) = &style.font { - s.insert("font", font.clone()); - } - chat.insert("style", s); - } - chat - }, - "narration" => compound! { - "translation_key" => self.narration.translation_key.clone(), - "parameters" => { - let mut parameters = Vec::new(); - if self.narration.parameters.sender { - parameters.push("sender".into()); - } - if self.narration.parameters.target { - parameters.push("target".into()); - } - if self.narration.parameters.content { - parameters.push("content".into()); - } - List::String(parameters) - }, - }, - } - } - } -} - -pub(crate) fn validate_chat_types(chat_types: &[ChatType]) -> anyhow::Result<()> { - ensure!( - !chat_types.is_empty(), - "at least one chat type must be present" - ); - - ensure!( - chat_types.len() <= u16::MAX as _, - "more than u16::MAX chat types present" - ); - - let mut names = HashSet::new(); - - for chat_type in chat_types { - ensure!( - names.insert(chat_type.name.clone()), - "chat type \"{}\" already exists", - chat_type.name - ); - } - - Ok(()) -} - impl Default for ChatType { fn default() -> Self { Self { - name: ident!("chat"), + name: ident!("chat").into(), chat: ChatTypeDecoration { translation_key: "chat.type.text".into(), style: None, @@ -189,3 +119,211 @@ pub struct ChatTypeParameters { target: bool, content: bool, } + +pub(crate) struct ChatTypePlugin; + +impl Plugin for ChatTypePlugin { + fn build(&self, app: &mut bevy_app::App) { + app.insert_resource(ChatTypeRegistry { + id_to_chat_type: vec![], + }) + .add_systems( + (update_chat_type_registry, remove_chat_types_from_registry) + .chain() + .in_base_set(CoreSet::PostUpdate) + .before(RegistryCodecSet), + ) + .add_startup_system(load_default_chat_types.in_base_set(StartupSet::PreStartup)); + } +} + +fn load_default_chat_types( + mut reg: ResMut, + codec: Res, + mut commands: Commands, +) { + let mut helper = move || { + for value in codec.registry(ChatTypeRegistry::KEY) { + let Some(Value::Compound(chat)) = value.element.get("chat") else { + bail!("missing chat type text decorations") + }; + + let chat_key = chat + .get("translation_key") + .and_then(|v| v.as_string()) + .context("invalid translation_key")? + .clone(); + + let chat_parameters = + if let Some(Value::List(List::String(params))) = chat.get("parameters") { + ChatTypeParameters { + sender: params.contains(&String::from("sender")), + target: params.contains(&String::from("target")), + content: params.contains(&String::from("content")), + } + } else { + bail!("missing chat type text parameters") + }; + + let Some(Value::Compound(narration)) = value.element.get("narration") else { + bail!("missing chat type text narration decorations") + }; + + let narration_key = narration + .get("translation_key") + .and_then(|v| v.as_string()) + .context("invalid translation_key")? + .clone(); + + let narration_parameters = + if let Some(Value::List(List::String(params))) = chat.get("parameters") { + ChatTypeParameters { + sender: params.contains(&String::from("sender")), + target: params.contains(&String::from("target")), + content: params.contains(&String::from("content")), + } + } else { + bail!("missing chat type narration parameters") + }; + + let entity = commands + .spawn(ChatType { + name: value.name.clone(), + chat: ChatTypeDecoration { + translation_key: chat_key, + // TODO: Add support for the chat type styling + style: None, + parameters: chat_parameters, + }, + narration: ChatTypeDecoration { + translation_key: narration_key, + style: None, + parameters: narration_parameters, + }, + }) + .id(); + + reg.id_to_chat_type.push(entity); + } + + Ok(()) + }; + + if let Err(e) = helper() { + error!("failed to load default chat types from registry codec: {e:#}"); + } +} + +/// Add new chat types to or update existing chat types in the registry. +fn update_chat_type_registry( + mut reg: ResMut, + mut codec: ResMut, + chat_types: Query<(Entity, &ChatType), Changed>, +) { + for (entity, chat_type) in &chat_types { + let chat_type_registry = codec.registry_mut(ChatTypeRegistry::KEY); + + let mut chat_text_compound = compound! { + "translation_key" => &chat_type.chat.translation_key, + "parameters" => { + let mut parameters = Vec::new(); + if chat_type.chat.parameters.sender { + parameters.push("sender".to_string()); + } + if chat_type.chat.parameters.target { + parameters.push("target".to_string()); + } + if chat_type.chat.parameters.content { + parameters.push("content".to_string()); + } + List::String(parameters) + }, + }; + + if let Some(style) = &chat_type.chat.style { + let mut s = Compound::new(); + if let Some(color) = style.color { + s.insert( + "color", + format!("#{:02x}{:02x}{:02x}", color.r, color.g, color.b), + ); + } + if let Some(bold) = style.bold { + s.insert("bold", bold); + } + if let Some(italic) = style.italic { + s.insert("italic", italic); + } + if let Some(underlined) = style.underlined { + s.insert("underlined", underlined); + } + if let Some(strikethrough) = style.strikethrough { + s.insert("strikethrough", strikethrough); + } + if let Some(obfuscated) = style.obfuscated { + s.insert("obfuscated", obfuscated); + } + if let Some(insertion) = &style.insertion { + s.insert("insertion", insertion.clone()); + } + if let Some(font) = &style.font { + s.insert("font", font.clone()); + } + chat_text_compound.insert("style", s); + } + + let chat_narration_compound = compound! { + "translation_key" => &chat_type.narration.translation_key, + "parameters" => { + let mut parameters = Vec::new(); + if chat_type.narration.parameters.sender { + parameters.push("sender".to_string()); + } + if chat_type.narration.parameters.target { + parameters.push("target".to_string()); + } + if chat_type.narration.parameters.content { + parameters.push("content".to_string()); + } + List::String(parameters) + }, + }; + + let chat_type_compound = compound! { + "chat" => chat_text_compound, + "narration" => chat_narration_compound, + }; + + if let Some(value) = chat_type_registry + .iter_mut() + .find(|v| v.name == chat_type.name) + { + value.name = chat_type.name.clone(); + value.element.merge(chat_type_compound); + } else { + chat_type_registry.push(RegistryValue { + name: chat_type.name.clone(), + element: chat_type_compound, + }); + reg.id_to_chat_type.push(entity); + } + } +} + +/// Remove deleted chat types from the registry. +fn remove_chat_types_from_registry( + mut chat_types: RemovedComponents, + mut reg: ResMut, + mut codec: ResMut, +) { + for chat_type in chat_types.iter() { + if let Some(idx) = reg + .id_to_chat_type + .iter() + .position(|entity| *entity == chat_type) + { + reg.id_to_chat_type.remove(idx); + codec.registry_mut(ChatTypeRegistry::KEY).remove(idx); + } + } +} diff --git a/crates/valence/src/client/event.rs b/crates/valence/src/client/event.rs index d1db77970..99d407f9e 100644 --- a/crates/valence/src/client/event.rs +++ b/crates/valence/src/client/event.rs @@ -1,3 +1,4 @@ +use std::borrow::Cow; use std::cmp; use anyhow::bail; @@ -8,7 +9,7 @@ use bevy_ecs::schedule::ScheduleLabel; use bevy_ecs::system::{SystemParam, SystemState}; use glam::{DVec3, Vec3}; use paste::paste; -use tracing::warn; +use tracing::{debug, warn}; use uuid::Uuid; use valence_protocol::block_pos::BlockPos; use valence_protocol::ident::Ident; @@ -17,7 +18,7 @@ use valence_protocol::packet::c2s::play::click_slot::{ClickMode, Slot}; use valence_protocol::packet::c2s::play::client_command::Action as ClientCommandAction; use valence_protocol::packet::c2s::play::client_settings::{ChatMode, DisplayedSkinParts, MainArm}; use valence_protocol::packet::c2s::play::player_action::Action as PlayerAction; -use valence_protocol::packet::c2s::play::player_interact::Interaction; +use valence_protocol::packet::c2s::play::player_interact_entity::EntityInteraction; use valence_protocol::packet::c2s::play::player_session::PlayerSessionData; use valence_protocol::packet::c2s::play::recipe_category_options::RecipeBookId; use valence_protocol::packet::c2s::play::update_command_block::Mode as CommandBlockMode; @@ -28,15 +29,19 @@ use valence_protocol::packet::c2s::play::update_structure_block::{ use valence_protocol::packet::c2s::play::{ AdvancementTabC2s, ClientStatusC2s, ResourcePackStatusC2s, UpdatePlayerAbilitiesC2s, }; +use valence_protocol::packet::s2c::play::InventoryS2c; use valence_protocol::packet::C2sPlayPacket; use valence_protocol::types::{Difficulty, Direction, Hand}; +use valence_protocol::var_int::VarInt; use super::{ CursorItem, KeepaliveState, PlayerActionSequence, PlayerInventoryState, TeleportState, }; use crate::client::Client; use crate::component::{Look, OnGround, Ping, Position}; -use crate::inventory::Inventory; +use crate::inventory::{Inventory, InventorySettings}; +use crate::packet::WritePacket; +use crate::prelude::OpenInventory; #[derive(Clone, Debug)] pub struct QueryBlockNbt { @@ -138,7 +143,7 @@ pub struct CloseHandledScreen { #[derive(Clone, Debug)] pub struct CustomPayload { pub client: Entity, - pub channel: Ident>, + pub channel: Ident, pub data: Box<[u8]>, } @@ -158,14 +163,14 @@ pub struct QueryEntityNbt { /// Left or right click interaction with an entity's hitbox. #[derive(Clone, Debug)] -pub struct PlayerInteract { +pub struct PlayerInteractEntity { pub client: Entity, /// The raw ID of the entity being interacted with. pub entity_id: i32, /// If the client was sneaking during the interaction. pub sneaking: bool, /// The kind of interaction that occurred. - pub interact: Interaction, + pub interact: EntityInteraction, } #[derive(Clone, Debug)] @@ -263,7 +268,7 @@ pub struct PickFromInventory { pub struct CraftRequest { pub client: Entity, pub window_id: i8, - pub recipe: Ident>, + pub recipe: Ident, pub make_all: bool, } @@ -351,7 +356,7 @@ pub struct RecipeCategoryOptions { #[derive(Clone, Debug)] pub struct RecipeBookData { pub client: Entity, - pub recipe_id: Ident>, + pub recipe_id: Ident, } #[derive(Clone, Debug)] @@ -392,7 +397,7 @@ pub struct ResourcePackStatusChange { #[derive(Clone, Debug)] pub struct OpenAdvancementTab { pub client: Entity, - pub tab_id: Ident>, + pub tab_id: Ident, } #[derive(Clone, Debug)] @@ -449,9 +454,9 @@ pub struct CreativeInventoryAction { pub struct UpdateJigsaw { pub client: Entity, pub position: BlockPos, - pub name: Ident>, - pub target: Ident>, - pub pool: Ident>, + pub name: Ident, + pub target: Ident, + pub pool: Ident, pub final_state: Box, pub joint_type: Box, } @@ -583,7 +588,7 @@ events! { QueryEntityNbt } 1 { - PlayerInteract + PlayerInteractEntity JigsawGenerating UpdateDifficultyLock PlayerMove @@ -673,6 +678,7 @@ pub(crate) struct EventLoopQuery { keepalive_state: &'static mut KeepaliveState, cursor_item: &'static mut CursorItem, inventory: &'static mut Inventory, + open_inventory: Option<&'static mut OpenInventory>, position: &'static mut Position, look: &'static mut Look, on_ground: &'static mut OnGround, @@ -684,10 +690,17 @@ pub(crate) struct EventLoopQuery { /// An exclusive system for running the event loop schedule. fn run_event_loop( world: &mut World, - state: &mut SystemState<(Query, ClientEvents, Commands)>, + state: &mut SystemState<( + Query, + ClientEvents, + Commands, + Query<&Inventory, Without>, + Res, + )>, mut clients_to_check: Local>, ) { - let (mut clients, mut events, mut commands) = state.get_mut(world); + let (mut clients, mut events, mut commands, mut inventories, inventory_settings) = + state.get_mut(world); update_all_event_buffers(&mut events); @@ -705,7 +718,7 @@ fn run_event_loop( q.client.dec.queue_bytes(bytes); - match handle_one_packet(&mut q, &mut events) { + match handle_one_packet(&mut q, &mut events, &mut inventories, &inventory_settings) { Ok(had_packet) => { if had_packet { // We decoded one packet, but there might be more. @@ -725,7 +738,8 @@ fn run_event_loop( while !clients_to_check.is_empty() { world.run_schedule(EventLoopSchedule); - let (mut clients, mut events, mut commands) = state.get_mut(world); + let (mut clients, mut events, mut commands, mut inventories, inventory_settings) = + state.get_mut(world); clients_to_check.retain(|&entity| { let Ok(mut q) = clients.get_mut(entity) else { @@ -733,7 +747,7 @@ fn run_event_loop( return false; }; - match handle_one_packet(&mut q, &mut events) { + match handle_one_packet(&mut q, &mut events, &mut inventories, &inventory_settings) { Ok(had_packet) => had_packet, Err(e) => { warn!("failed to dispatch events for client {:?}: {e:?}", q.entity); @@ -750,6 +764,8 @@ fn run_event_loop( fn handle_one_packet( q: &mut EventLoopQueryItem, events: &mut ClientEvents, + inventories: &mut Query<&Inventory, Without>, + inventory_settings: &Res, ) -> anyhow::Result { let Some(pkt) = q.client.dec.try_next_packet::()? else { // No packets to decode. @@ -853,7 +869,57 @@ fn handle_one_packet( }); } C2sPlayPacket::ClickSlotC2s(p) => { - if p.slot_idx < 0 { + let open_inv = q + .open_inventory + .as_ref() + .and_then(|open| inventories.get_mut(open.entity).ok()); + if let Err(msg) = + crate::inventory::validate_click_slot_impossible(&p, &q.inventory, open_inv) + { + debug!( + "client {:#?} invalid click slot packet: \"{}\" {:#?}", + q.entity, msg, p + ); + let inventory = open_inv.unwrap_or(&q.inventory); + q.client.write_packet(&InventoryS2c { + window_id: if open_inv.is_some() { + q.player_inventory_state.window_id + } else { + 0 + }, + state_id: VarInt(q.player_inventory_state.state_id.0), + slots: Cow::Borrowed(inventory.slot_slice()), + carried_item: Cow::Borrowed(&q.cursor_item.0), + }); + return Ok(true); + } + if inventory_settings.enable_item_dupe_check { + if let Err(msg) = crate::inventory::validate_click_slot_item_duplication( + &p, + &q.inventory, + open_inv, + &q.cursor_item, + ) { + debug!( + "client {:#?} click slot packet tried to incorrectly modify items: \"{}\" \ + {:#?}", + q.entity, msg, p + ); + let inventory = open_inv.unwrap_or(&q.inventory); + q.client.write_packet(&InventoryS2c { + window_id: if open_inv.is_some() { + q.player_inventory_state.window_id + } else { + 0 + }, + state_id: VarInt(q.player_inventory_state.state_id.0), + slots: Cow::Borrowed(inventory.slot_slice()), + carried_item: Cow::Borrowed(&q.cursor_item.0), + }); + return Ok(true); + } + } + if p.slot_idx < 0 && p.mode == ClickMode::Click { if let Some(stack) = q.cursor_item.0.take() { events.2.drop_item_stack.send(DropItemStack { client: entity, @@ -922,8 +988,8 @@ fn handle_one_packet( entity_id: p.entity_id.0, }); } - C2sPlayPacket::PlayerInteractC2s(p) => { - events.1.player_interact.send(PlayerInteract { + C2sPlayPacket::PlayerInteractEntityC2s(p) => { + events.1.player_interact_entity.send(PlayerInteractEntity { client: entity, entity_id: p.entity_id.0, sneaking: p.sneaking, @@ -958,7 +1024,7 @@ fn handle_one_packet( locked: p.locked, }); } - C2sPlayPacket::PositionAndOnGroundC2s(p) => { + C2sPlayPacket::PositionAndOnGround(p) => { if q.teleport_state.pending_teleports != 0 { return Ok(false); } @@ -975,7 +1041,7 @@ fn handle_one_packet( q.teleport_state.synced_pos = p.position.into(); q.on_ground.0 = p.on_ground; } - C2sPlayPacket::FullC2s(p) => { + C2sPlayPacket::Full(p) => { if q.teleport_state.pending_teleports != 0 { return Ok(false); } @@ -996,7 +1062,7 @@ fn handle_one_packet( q.teleport_state.synced_look.pitch = p.pitch; q.on_ground.0 = p.on_ground; } - C2sPlayPacket::LookAndOnGroundC2s(p) => { + C2sPlayPacket::LookAndOnGround(p) => { if q.teleport_state.pending_teleports != 0 { return Ok(false); } @@ -1015,7 +1081,7 @@ fn handle_one_packet( q.teleport_state.synced_look.pitch = p.pitch; q.on_ground.0 = p.on_ground; } - C2sPlayPacket::OnGroundOnlyC2s(p) => { + C2sPlayPacket::OnGroundOnly(p) => { if q.teleport_state.pending_teleports != 0 { return Ok(false); } diff --git a/crates/valence/src/config.rs b/crates/valence/src/config.rs index 8cf444b4f..d5e1c1960 100644 --- a/crates/valence/src/config.rs +++ b/crates/valence/src/config.rs @@ -9,9 +9,6 @@ use tracing::error; use uuid::Uuid; use valence_protocol::text::Text; -use crate::biome::Biome; -use crate::chat_type::ChatType; -use crate::dimension::Dimension; use crate::server::{NewClientInfo, SharedServer}; #[derive(Clone)] @@ -102,49 +99,6 @@ pub struct ServerPlugin { /// An unspecified value is used that should be adequate for most /// situations. This default may change in future versions. pub outgoing_capacity: usize, - /// The list of [`Dimension`]s usable on the server. - /// - /// The dimensions returned by [`ServerPlugin::dimensions`] will be in the - /// same order as this `Vec`. - /// - /// The number of elements in the `Vec` must be in `1..=u16::MAX`. - /// Additionally, the documented requirements on the fields of [`Dimension`] - /// must be met. - /// - /// # Default Value - /// - /// `vec![Dimension::default()]` - pub dimensions: Arc<[Dimension]>, - /// The list of [`Biome`]s usable on the server. - /// - /// The biomes returned by [`SharedServer::biomes`] will be in the same - /// order as this `Vec`. - /// - /// The number of elements in the `Vec` must be in `1..=u16::MAX`. - /// Additionally, the documented requirements on the fields of [`Biome`] - /// must be met. - /// - /// **NOTE**: As of 1.19.2, there is a bug in the client which prevents - /// joining the game when a biome named "minecraft:plains" is not present. - /// Ensure there is a biome named "plains". - /// - /// # Default Value - /// - /// `vec![Biome::default()]`. - pub biomes: Arc<[Biome]>, - /// The list of [`ChatType`]s usable on the server. - /// - /// The chat types returned by [`ServerPlugin::chat_types`] will be in the - /// same order as this `Vec`. - /// - /// The number of elements in the `Vec` must be in `1..=u16::MAX`. - /// Additionally, the documented requirements on the fields of [`ChatType`] - /// must be met. - /// - /// # Default Value - /// - /// `vec![ChatType::default()]` - pub chat_types: Arc<[ChatType]>, } impl ServerPlugin { @@ -162,9 +116,6 @@ impl ServerPlugin { compression_threshold: Some(256), incoming_capacity: 2097152, // 2 MiB outgoing_capacity: 8388608, // 8 MiB - dimensions: [Dimension::default()].as_slice().into(), - biomes: [Biome::default()].as_slice().into(), - chat_types: [ChatType::default()].as_slice().into(), } } @@ -223,27 +174,6 @@ impl ServerPlugin { self.outgoing_capacity = outgoing_capacity; self } - - /// See [`Self::dimensions`]. - #[must_use] - pub fn with_dimensions(mut self, dimensions: impl Into>) -> Self { - self.dimensions = dimensions.into(); - self - } - - /// See [`Self::biomes`]. - #[must_use] - pub fn with_biomes(mut self, biomes: impl Into>) -> Self { - self.biomes = biomes.into(); - self - } - - /// See [`Self::chat_types`]. - #[must_use] - pub fn with_chat_types(mut self, chat_types: impl Into>) -> Self { - self.chat_types = chat_types.into(); - self - } } impl Default for ServerPlugin { diff --git a/crates/valence/src/lib.rs b/crates/valence/src/lib.rs index f01a3dc03..84bc07a5d 100644 --- a/crates/valence/src/lib.rs +++ b/crates/valence/src/lib.rs @@ -38,6 +38,7 @@ pub mod inventory; pub mod packet; pub mod player_list; pub mod player_textures; +pub mod registry_codec; pub mod secure_chat; pub mod server; #[cfg(any(test, doctest))] @@ -50,18 +51,21 @@ pub mod prelude { pub use async_trait::async_trait; pub use bevy_app::prelude::*; pub use bevy_ecs::prelude::*; - pub use biome::{Biome, BiomeId}; + pub use biome::{Biome, BiomeId, BiomeRegistry}; + pub use chat_type::{ChatType, ChatTypeId, ChatTypeRegistry}; pub use client::event::{EventLoopSchedule, EventLoopSet}; pub use client::*; pub use component::*; pub use config::{ AsyncCallbacks, ConnectionMode, PlayerSampleEntry, ServerListPing, ServerPlugin, }; - pub use dimension::{Dimension, DimensionId}; + pub use dimension::{DimensionType, DimensionTypeRegistry}; pub use entity::{EntityAnimation, EntityKind, EntityManager, EntityStatus, HeadYaw}; pub use glam::DVec3; pub use instance::{Block, BlockMut, BlockRef, Chunk, Instance}; - pub use inventory::{Inventory, InventoryKind, OpenInventory}; + pub use inventory::{ + Inventory, InventoryKind, InventoryWindow, InventoryWindowMut, OpenInventory, + }; pub use player_list::{PlayerList, PlayerListEntry}; pub use protocol::block::{BlockState, PropName, PropValue}; pub use protocol::ident::Ident; diff --git a/crates/valence/src/secure_chat.rs b/crates/valence/src/secure_chat.rs index c25bbd6ad..6bc52b9e1 100644 --- a/crates/valence/src/secure_chat.rs +++ b/crates/valence/src/secure_chat.rs @@ -531,6 +531,7 @@ fn handle_message_events( }; let Some(session) = &state.session else { + warn!("Player `{}` doesn't have a chat session", username.0); commands.add(DisconnectClient { client: message.client, reason: Text::translate(CHAT_DISABLED_MISSING_PROFILE_KEY, []) @@ -540,6 +541,7 @@ fn handle_message_events( // Verify that the player's session has not expired if session.is_expired() { + warn!("Player `{}` has an expired chat session", username.0); commands.add(DisconnectClient { client: message.client, reason: Text::translate(CHAT_DISABLED_EXPIRED_PROFILE_KEY, []), @@ -549,6 +551,7 @@ fn handle_message_events( // Verify that the chat message is signed let Some(message_signature) = &message.signature else { + warn!("Received unsigned chat message from `{}`", username.0); commands.add(DisconnectClient { client: message.client, reason: Text::translate(MULTIPLAYER_DISCONNECT_UNSIGNED_CHAT, []) @@ -585,6 +588,7 @@ fn handle_message_events( ) .is_err() { + warn!("Failed to verify chat message from `{}`", username.0); commands.add(DisconnectClient { client: message.client, reason: Text::translate(MULTIPLAYER_DISCONNECT_UNSIGNED_CHAT, []), diff --git a/crates/valence/src/server.rs b/crates/valence/src/server.rs index eb1a57681..92cd07430 100644 --- a/crates/valence/src/server.rs +++ b/crates/valence/src/server.rs @@ -1,4 +1,3 @@ -use std::iter::FusedIterator; use std::net::{IpAddr, SocketAddr}; use std::ops::Deref; use std::sync::Arc; @@ -14,22 +13,21 @@ use rsa::{PublicKeyParts, RsaPrivateKey}; use tokio::runtime::{Handle, Runtime}; use tokio::sync::Semaphore; use uuid::Uuid; -use valence_nbt::{compound, Compound, List}; -use valence_protocol::ident; use valence_protocol::types::Property; -use crate::biome::{validate_biomes, Biome, BiomeId}; -use crate::chat_type::{validate_chat_types, ChatType, ChatTypeId}; +use crate::biome::BiomePlugin; +use crate::chat_type::ChatTypePlugin; use crate::client::event::EventLoopSet; use crate::client::{ClientBundle, ClientPlugin}; use crate::config::{AsyncCallbacks, ConnectionMode, ServerPlugin}; -use crate::dimension::{validate_dimensions, Dimension, DimensionId}; +use crate::dimension::DimensionPlugin; use crate::entity::EntityPlugin; -use crate::instance::{Instance, InstancePlugin}; -use crate::inventory::InventoryPlugin; +use crate::instance::InstancePlugin; +use crate::inventory::{InventoryPlugin, InventorySettings}; use crate::player_list::PlayerListPlugin; use crate::prelude::event::ClientEventPlugin; use crate::prelude::ComponentPlugin; +use crate::registry_codec::RegistryCodecPlugin; use crate::server::connect::do_accept_loop; use crate::weather::WeatherPlugin; @@ -84,12 +82,6 @@ struct SharedServerInner { /// Holding a runtime handle is not enough to keep tokio working. We need /// to store the runtime here so we don't drop it. _tokio_runtime: Option, - dimensions: Arc<[Dimension]>, - biomes: Arc<[Biome]>, - chat_types: Arc<[ChatType]>, - /// Contains info about dimensions, biomes, and chats. - /// Sent to all clients when joining. - registry_codec: Compound, /// Sender for new clients past the login stage. new_clients_send: Sender, /// Receiver for new clients past the login stage. @@ -107,12 +99,6 @@ struct SharedServerInner { } impl SharedServer { - /// Creates a new [`Instance`] component with the given dimension. - #[must_use] - pub fn new_instance(&self, dimension: DimensionId) -> Instance { - Instance::new(dimension, self) - } - /// Gets the socket address this server is bound to. pub fn address(&self) -> SocketAddr { self.0.address @@ -153,72 +139,6 @@ impl SharedServer { pub fn tokio_handle(&self) -> &Handle { &self.0.tokio_handle } - - /// Obtains a [`Dimension`] by using its corresponding [`DimensionId`]. - #[track_caller] - pub fn dimension(&self, id: DimensionId) -> &Dimension { - self.0 - .dimensions - .get(id.0 as usize) - .expect("invalid dimension ID") - } - - /// Returns an iterator over all added dimensions and their associated - /// [`DimensionId`]. - pub fn dimensions(&self) -> impl FusedIterator + Clone { - self.0 - .dimensions - .iter() - .enumerate() - .map(|(i, d)| (DimensionId(i as u16), d)) - } - - /// Obtains a [`Biome`] by using its corresponding [`BiomeId`]. - #[track_caller] - pub fn biome(&self, id: BiomeId) -> &Biome { - self.0.biomes.get(id.0 as usize).expect("invalid biome ID") - } - - /// Returns an iterator over all added biomes and their associated - /// [`BiomeId`] in ascending order. - pub fn biomes( - &self, - ) -> impl ExactSizeIterator + DoubleEndedIterator + FusedIterator + Clone - { - self.0 - .biomes - .iter() - .enumerate() - .map(|(i, b)| (BiomeId(i as u16), b)) - } - - /// Obtains a [`ChatType`] by using its corresponding [`ChatTypeId`]. - #[track_caller] - pub fn chat_type(&self, id: ChatTypeId) -> &ChatType { - self.0 - .chat_types - .get(id.0 as usize) - .expect("invalid chat type ID") - } - - /// Returns an iterator over all added chat types and their associated - /// [`ChatTypeId`] in ascending order. - pub fn chat_types( - &self, - ) -> impl ExactSizeIterator - + DoubleEndedIterator - + FusedIterator - + Clone { - self.0 - .chat_types - .iter() - .enumerate() - .map(|(i, t)| (ChatTypeId(i as u16), t)) - } - - pub(crate) fn registry_codec(&self) -> &Compound { - &self.0.registry_codec - } } /// Contains information about a new client joining the server. @@ -269,13 +189,6 @@ pub fn build_plugin( None => plugin.tokio_handle.clone().unwrap(), }; - validate_dimensions(&plugin.dimensions)?; - validate_biomes(&plugin.biomes)?; - validate_chat_types(&plugin.chat_types)?; - - let registry_codec = - make_registry_codec(&plugin.dimensions, &plugin.biomes, &plugin.chat_types); - let (new_clients_send, new_clients_recv) = flume::bounded(64); let shared = SharedServer(Arc::new(SharedServerInner { @@ -288,10 +201,6 @@ pub fn build_plugin( outgoing_capacity: plugin.outgoing_capacity, tokio_handle, _tokio_runtime: runtime, - dimensions: plugin.dimensions.clone(), - biomes: plugin.biomes.clone(), - chat_types: plugin.chat_types.clone(), - registry_codec, new_clients_send, new_clients_recv, connection_sema: Arc::new(Semaphore::new(plugin.max_connections)), @@ -332,6 +241,7 @@ pub fn build_plugin( // Insert resources. app.insert_resource(server); + app.insert_resource(InventorySettings::default()); // Make the app loop forever at the configured TPS. { @@ -359,7 +269,11 @@ pub fn build_plugin( app.add_system(increment_tick_counter.in_base_set(CoreSet::Last)); // Add internal plugins. - app.add_plugin(ComponentPlugin) + app.add_plugin(RegistryCodecPlugin) + .add_plugin(BiomePlugin) + .add_plugin(ChatTypePlugin) + .add_plugin(DimensionPlugin) + .add_plugin(ComponentPlugin) .add_plugin(ClientPlugin) .add_plugin(ClientEventPlugin) .add_plugin(EntityPlugin) @@ -368,62 +282,9 @@ pub fn build_plugin( .add_plugin(PlayerListPlugin) .add_plugin(WeatherPlugin); - /* - println!( - "{}", - bevy_mod_debugdump::schedule_graph_dot( - app, - CoreSchedule::Main, - &bevy_mod_debugdump::schedule_graph::Settings { - ambiguity_enable: false, - ..Default::default() - }, - ) - ); - */ - Ok(()) } fn increment_tick_counter(mut server: ResMut) { server.current_tick += 1; } - -fn make_registry_codec( - dimensions: &[Dimension], - biomes: &[Biome], - chat_types: &[ChatType], -) -> Compound { - let dimensions = dimensions - .iter() - .enumerate() - .map(|(id, dim)| dim.to_dimension_registry_item(id as i32)) - .collect(); - - let biomes = biomes - .iter() - .enumerate() - .map(|(id, biome)| biome.to_biome_registry_item(id as i32)) - .collect(); - - let chat_types = chat_types - .iter() - .enumerate() - .map(|(id, chat_type)| chat_type.to_chat_type_registry_item(id as i32)) - .collect(); - - compound! { - ident!("dimension_type") => compound! { - "type" => ident!("dimension_type"), - "value" => List::Compound(dimensions), - }, - ident!("worldgen/biome") => compound! { - "type" => ident!("worldgen/biome"), - "value" => List::Compound(biomes), - }, - ident!("chat_type") => compound! { - "type" => ident!("chat_type"), - "value" => List::Compound(chat_types), - }, - } -} diff --git a/crates/valence_protocol/src/packet/s2c/play/chat_message.rs b/crates/valence_protocol/src/packet/s2c/play/chat_message.rs index 3700b6d97..c4690f99e 100644 --- a/crates/valence_protocol/src/packet/s2c/play/chat_message.rs +++ b/crates/valence_protocol/src/packet/s2c/play/chat_message.rs @@ -1,5 +1,4 @@ use std::borrow::Cow; -use std::io::Write; use uuid::Uuid; @@ -8,7 +7,7 @@ use crate::types::MessageSignature; use crate::var_int::VarInt; use crate::{Decode, Encode}; -#[derive(Clone, PartialEq, Debug)] +#[derive(Clone, PartialEq, Debug, Encode, Decode)] pub struct ChatMessageS2c<'a> { pub sender: Uuid, pub index: VarInt, @@ -30,41 +29,3 @@ pub enum MessageFilterType { FullyFiltered, PartiallyFiltered { mask: Vec }, } - -impl<'a> Encode for ChatMessageS2c<'a> { - fn encode(&self, mut w: impl Write) -> anyhow::Result<()> { - self.sender.encode(&mut w)?; - self.index.encode(&mut w)?; - self.message_signature.encode(&mut w)?; - self.message.encode(&mut w)?; - self.time_stamp.encode(&mut w)?; - self.salt.encode(&mut w)?; - self.previous_messages.encode(&mut w)?; - self.unsigned_content.encode(&mut w)?; - self.filter_type.encode(&mut w)?; - self.chat_type.encode(&mut w)?; - self.network_name.encode(&mut w)?; - self.network_target_name.encode(&mut w)?; - - Ok(()) - } -} - -impl<'a> Decode<'a> for ChatMessageS2c<'a> { - fn decode(r: &mut &'a [u8]) -> anyhow::Result { - Ok(Self { - sender: Uuid::decode(r)?, - index: VarInt::decode(r)?, - message_signature: Option::<&'a [u8; 256]>::decode(r)?, - message: <&str>::decode(r)?, - time_stamp: u64::decode(r)?, - salt: u64::decode(r)?, - previous_messages: Vec::::decode(r)?, - unsigned_content: Option::>::decode(r)?, - filter_type: MessageFilterType::decode(r)?, - chat_type: VarInt::decode(r)?, - network_name: >::decode(r)?, - network_target_name: Option::>::decode(r)?, - }) - } -} From 6b81ba33a186e174a78fd4b5b95371df7435e9ba Mon Sep 17 00:00:00 2001 From: guac420 <60993440+guac42@users.noreply.github.com> Date: Thu, 6 Apr 2023 18:48:20 -0700 Subject: [PATCH 09/32] Formatting --- crates/valence/examples/chat.rs | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/crates/valence/examples/chat.rs b/crates/valence/examples/chat.rs index 54befbd6e..e83aa8960 100644 --- a/crates/valence/examples/chat.rs +++ b/crates/valence/examples/chat.rs @@ -20,13 +20,7 @@ pub fn main() { .add_startup_system(setup) .add_system(init_clients) .add_system(despawn_disconnected_clients) - .add_systems( - ( - default_event_handler, - handle_command_events - ) - .in_schedule(EventLoopSchedule), - ) + .add_systems((default_event_handler, handle_command_events).in_schedule(EventLoopSchedule)) .add_systems(PlayerList::default_systems()) .run(); } From 08699f59f9dee3b65074f8cfebe6d22b639e14db Mon Sep 17 00:00:00 2001 From: Guac <60993440+guac42@users.noreply.github.com> Date: Sun, 9 Apr 2023 00:57:19 -0700 Subject: [PATCH 10/32] Fix merge conflicts --- crates/valence/examples/chat.rs | 7 ++-- crates/valence/src/client/misc.rs | 54 +++++++++++++++++++++++++++++-- crates/valence/src/secure_chat.rs | 31 +++--------------- 3 files changed, 61 insertions(+), 31 deletions(-) diff --git a/crates/valence/examples/chat.rs b/crates/valence/examples/chat.rs index e83aa8960..9e3911b05 100644 --- a/crates/valence/examples/chat.rs +++ b/crates/valence/examples/chat.rs @@ -1,8 +1,9 @@ #![allow(clippy::type_complexity)] use tracing::{warn, Level}; -use valence::client::event::CommandExecution; -use valence::client::{default_event_handler, despawn_disconnected_clients}; +use valence::client::despawn_disconnected_clients; +// TODO: Add CommandExecution event +use valence::client::misc::CommandExecution; use valence::entity::player::PlayerEntityBundle; use valence::prelude::*; use valence::secure_chat::SecureChatPlugin; @@ -20,7 +21,7 @@ pub fn main() { .add_startup_system(setup) .add_system(init_clients) .add_system(despawn_disconnected_clients) - .add_systems((default_event_handler, handle_command_events).in_schedule(EventLoopSchedule)) + .add_system(handle_command_events.in_schedule(EventLoopSchedule)) .add_systems(PlayerList::default_systems()) .run(); } diff --git a/crates/valence/src/client/misc.rs b/crates/valence/src/client/misc.rs index cda4b6519..09511197d 100644 --- a/crates/valence/src/client/misc.rs +++ b/crates/valence/src/client/misc.rs @@ -2,9 +2,10 @@ use bevy_app::prelude::*; use bevy_ecs::prelude::*; use glam::Vec3; use valence_protocol::block_pos::BlockPos; +use valence_protocol::packet::c2s::play::player_session::PlayerSessionData; use valence_protocol::packet::c2s::play::{ - ChatMessageC2s, ClientStatusC2s, HandSwingC2s, PlayerInteractBlockC2s, PlayerInteractItemC2s, - ResourcePackStatusC2s, + ChatMessageC2s, ClientStatusC2s, CommandExecutionC2s, HandSwingC2s, MessageAcknowledgmentC2s, + PlayerInteractBlockC2s, PlayerInteractItemC2s, PlayerSessionC2s, ResourcePackStatusC2s, }; use valence_protocol::types::{Direction, Hand}; @@ -14,7 +15,10 @@ use crate::event_loop::{EventLoopSchedule, EventLoopSet, PacketEvent}; pub(super) fn build(app: &mut App) { app.add_event::() .add_event::() + .add_event::() .add_event::() + .add_event::() + .add_event::() .add_event::() .add_event::() .add_event::() @@ -48,11 +52,34 @@ pub struct InteractBlock { pub sequence: i32, } +#[derive(Clone, Debug)] +pub struct CommandExecution { + pub client: Entity, + pub command: Box, + pub timestamp: u64, +} + #[derive(Clone, Debug)] pub struct ChatMessage { pub client: Entity, pub message: Box, pub timestamp: u64, + pub salt: u64, + pub signature: Option>, + pub message_index: i32, + pub acknowledgements: [u8; 3], +} + +#[derive(Copy, Clone, Debug)] +pub struct MessageAcknowledgment { + pub client: Entity, + pub message_index: i32, +} + +#[derive(Clone, Debug)] +pub struct PlayerSession { + pub client: Entity, + pub session_data: PlayerSessionData, } #[derive(Copy, Clone, Debug)] @@ -89,7 +116,10 @@ fn handle_misc_packets( mut clients: Query<&mut ActionSequence>, mut hand_swing_events: EventWriter, mut interact_block_events: EventWriter, + mut command_execution_events: EventWriter, mut chat_message_events: EventWriter, + mut message_acknowledgement_events: EventWriter, + mut player_session_events: EventWriter, mut respawn_events: EventWriter, mut request_stats_events: EventWriter, mut resource_pack_status_change_events: EventWriter, @@ -120,11 +150,31 @@ fn handle_misc_packets( } // TODO + } else if let Some(pkt) = packet.decode::() { + command_execution_events.send(CommandExecution { + client: packet.client, + command: pkt.command.into(), + timestamp: pkt.timestamp, + }); } else if let Some(pkt) = packet.decode::() { chat_message_events.send(ChatMessage { client: packet.client, message: pkt.message.into(), timestamp: pkt.timestamp, + salt: pkt.salt, + signature: pkt.signature.copied().map(Box::new), + message_index: pkt.message_index.0, + acknowledgements: pkt.acknowledgement, + }); + } else if let Some(pkt) = packet.decode::() { + message_acknowledgement_events.send(MessageAcknowledgment { + client: packet.client, + message_index: pkt.message_index.0, + }); + } else if let Some(pkt) = packet.decode::() { + player_session_events.send(PlayerSession { + client: packet.client, + session_data: pkt.0.into_owned(), }); } else if let Some(pkt) = packet.decode::() { match pkt { diff --git a/crates/valence/src/secure_chat.rs b/crates/valence/src/secure_chat.rs index 6bc52b9e1..3d2320ea6 100644 --- a/crates/valence/src/secure_chat.rs +++ b/crates/valence/src/secure_chat.rs @@ -23,7 +23,8 @@ use valence_protocol::translation_key::{ use valence_protocol::types::MessageSignature; use valence_protocol::var_int::VarInt; -use crate::client::event::{ChatMessage, ClientSettings, MessageAcknowledgment, PlayerSession}; +use crate::client::misc::{ChatMessage, MessageAcknowledgment, PlayerSession}; +use crate::client::settings::ClientSettings; use crate::client::{Client, DisconnectClient, FlushPacketsSet}; use crate::component::{UniqueId, Username}; use crate::instance::Instance; @@ -45,7 +46,6 @@ impl MojangServicesState { #[derive(Debug, Component)] struct ChatState { last_message_timestamp: u64, - chat_mode: ChatMode, validator: AcknowledgementValidator, chain: MessageChain, signature_storage: MessageSignatureStorage, @@ -59,7 +59,6 @@ impl Default for ChatState { .duration_since(SystemTime::UNIX_EPOCH) .expect("Unable to get Unix time") .as_millis() as u64, - chat_mode: ChatMode::Enabled, validator: AcknowledgementValidator::new(), chain: MessageChain::new(), signature_storage: MessageSignatureStorage::new(), @@ -320,9 +319,6 @@ impl Plugin for SecureChatPlugin { handle_message_acknowledgement .after(init_chat_states) .before(handle_message_events), - handle_chat_settings_event - .after(init_chat_states) - .before(handle_message_events), handle_message_events.after(init_chat_states), ) .in_base_set(CoreSet::PostUpdate) @@ -463,7 +459,7 @@ fn handle_message_acknowledgement( } fn handle_message_events( - mut clients: Query<(&Username, &mut Client)>, + mut clients: Query<(&Username, &ClientSettings, &mut Client)>, mut states: Query<&mut ChatState>, mut messages: EventReader, mut instances: Query<&mut Instance>, @@ -472,7 +468,7 @@ fn handle_message_events( let mut instance = instances.single_mut(); for message in messages.iter() { - let Ok((username, mut client)) = clients.get_mut(message.client) else { + let Ok((username, settings, mut client)) = clients.get_mut(message.client) else { warn!("Unable to find client for message '{:?}'", message); continue; }; @@ -483,7 +479,7 @@ fn handle_message_events( }; // Ensure that the client isn't sending messages while their chat is hidden - if state.chat_mode == ChatMode::Hidden { + if settings.chat_mode == ChatMode::Hidden { client.send_message(Text::translate(CHAT_DISABLED_OPTIONS, []).color(Color::RED)); continue; } @@ -641,20 +637,3 @@ fn handle_message_events( } } } - -fn handle_chat_settings_event( - mut states: Query<&mut ChatState>, - mut settings: EventReader, -) { - for ClientSettings { - client, chat_mode, .. - } in settings.iter() - { - let Ok(mut state) = states.get_component_mut::(*client) else { - warn!("Unable to find chat state for client"); - continue; - }; - - state.chat_mode = *chat_mode; - } -} From 6fe20f9c680b625da32daa16f123e25a5c6d2028 Mon Sep 17 00:00:00 2001 From: Guac <60993440+guac42@users.noreply.github.com> Date: Mon, 10 Apr 2023 12:51:07 -0700 Subject: [PATCH 11/32] Fix merge conflicts -final tests still need to be done --- crates/valence/src/player_list.rs | 27 ++++++++--- crates/valence/src/secure_chat.rs | 75 +++++++++++-------------------- 2 files changed, 48 insertions(+), 54 deletions(-) diff --git a/crates/valence/src/player_list.rs b/crates/valence/src/player_list.rs index bb4aaedd8..c8901954c 100644 --- a/crates/valence/src/player_list.rs +++ b/crates/valence/src/player_list.rs @@ -2,6 +2,7 @@ use std::borrow::Cow; use bevy_app::prelude::*; use bevy_ecs::prelude::*; +use rsa::RsaPublicKey; use uuid::Uuid; use valence_protocol::packet::c2s::play::player_session::PlayerSessionData; use valence_protocol::packet::s2c::play::player_list::{Actions, Entry, PlayerListS2c}; @@ -105,7 +106,6 @@ pub struct PlayerListEntryBundle { pub ping: Ping, pub display_name: DisplayName, pub listed: Listed, - pub chat_session: ChatSession, } /// Marker component for player list entries. @@ -120,15 +120,20 @@ pub struct DisplayName(pub Option); #[derive(Component, Copy, Clone, Debug)] pub struct Listed(pub bool); -#[derive(Component, Clone, Default, Debug)] -pub struct ChatSession(pub PlayerSessionData); - impl Default for Listed { fn default() -> Self { Self(true) } } +/// Contains information for the player's chat message verification. +/// Not required. +#[derive(Component, Clone, Debug)] +pub struct ChatSession { + pub public_key: RsaPublicKey, + pub session_data: PlayerSessionData, +} + fn update_header_footer(player_list: ResMut, server: Res) { if player_list.changed_header_or_footer { let player_list = player_list.into_inner(); @@ -261,6 +266,7 @@ fn update_entries( Ref, Ref, Ref, + Option>, ), ( With, @@ -272,6 +278,7 @@ fn update_entries( Changed, Changed, Changed, + Changed, )>, ), >, @@ -286,7 +293,7 @@ fn update_entries( &mut player_list.scratch, ); - for (uuid, username, props, game_mode, ping, display_name, listed) in &entries { + for (uuid, username, props, game_mode, ping, display_name, listed, chat_session) in &entries { let mut actions = Actions::new(); // Did a change occur that would force us to overwrite the entry? This also adds @@ -309,6 +316,10 @@ fn update_entries( if listed.0 { actions.set_update_listed(true); } + + if chat_session.is_some() { + actions.set_initialize_chat(true); + } } else { if game_mode.is_changed() { actions.set_update_game_mode(true); @@ -326,6 +337,10 @@ fn update_entries( actions.set_update_listed(true); } + if matches!(&chat_session, Some(session) if session.is_changed()) { + actions.set_initialize_chat(true); + } + debug_assert_ne!(u8::from(actions), 0); } @@ -333,7 +348,7 @@ fn update_entries( player_uuid: uuid.0, username: &username.0, properties: (&props.0).into(), - chat_data: Some(), + chat_data: chat_session.map(|s| s.session_data.clone().into()), listed: listed.0, ping: ping.0, game_mode: (*game_mode).into(), diff --git a/crates/valence/src/secure_chat.rs b/crates/valence/src/secure_chat.rs index 3d2320ea6..89923be89 100644 --- a/crates/valence/src/secure_chat.rs +++ b/crates/valence/src/secure_chat.rs @@ -28,7 +28,7 @@ use crate::client::settings::ClientSettings; use crate::client::{Client, DisconnectClient, FlushPacketsSet}; use crate::component::{UniqueId, Username}; use crate::instance::Instance; -use crate::player_list::PlayerList; +use crate::player_list::{ChatSession, PlayerListEntry}; const MOJANG_KEY_DATA: &[u8] = include_bytes!("../../../assets/yggdrasil_session_pubkey.der"); @@ -49,7 +49,6 @@ struct ChatState { validator: AcknowledgementValidator, chain: MessageChain, signature_storage: MessageSignatureStorage, - session: Option, } impl Default for ChatState { @@ -62,7 +61,6 @@ impl Default for ChatState { validator: AcknowledgementValidator::new(), chain: MessageChain::new(), signature_storage: MessageSignatureStorage::new(), - session: None, } } } @@ -286,22 +284,6 @@ impl MessageSignatureStorage { } } -#[derive(Clone, Debug)] -struct ChatSession { - expires_at: i64, - public_key: RsaPublicKey, -} - -impl ChatSession { - pub fn is_expired(&self) -> bool { - SystemTime::now() - .duration_since(SystemTime::UNIX_EPOCH) - .expect("Unable to get Unix time") - .as_millis() - >= self.expires_at as u128 - } -} - pub struct SecureChatPlugin; impl Plugin for SecureChatPlugin { @@ -335,21 +317,13 @@ fn init_chat_states(clients: Query>, mut commands: Command fn handle_session_events( services_state: Res, - player_list: ResMut, - mut clients: Query<(&UniqueId, &Username, &mut ChatState)>, + mut clients: Query<(&UniqueId, &Username, &mut ChatState), With>, mut sessions: EventReader, mut commands: Commands, ) { - let pl = player_list.into_inner(); - for session in sessions.iter() { let Ok((uuid, username, mut state)) = clients.get_mut(session.client) else { - warn!("Unable to find client for session"); - continue; - }; - - let Some(player_entry) = pl.get_mut(uuid.0) else { - warn!("Unable to find '{}' in the player list", username.0); + warn!("Unable to find client in player list for session"); continue; }; @@ -407,9 +381,12 @@ fn handle_session_events( sender: uuid.0, session_id: session.session_data.session_id, }); - state.session = Some(ChatSession { - expires_at: session.session_data.expires_at, + + // Add the chat session data to player. + // The player list will then send this new session data to the other clients. + commands.entity(session.client).insert(ChatSession { public_key, + session_data: session.session_data.clone(), }); } else { // This shouldn't happen considering that it is highly unlikely that Mojang @@ -424,10 +401,6 @@ fn handle_session_events( ), }); } - - // Update the player list with the new session data - // The player list will then send this new session data to the other clients - player_entry.set_chat_data(Some(session.session_data.clone())); } } @@ -459,7 +432,8 @@ fn handle_message_acknowledgement( } fn handle_message_events( - mut clients: Query<(&Username, &ClientSettings, &mut Client)>, + mut clients: Query<(&Username, &ClientSettings, &mut Client), With>, + sessions: Query<&ChatSession, With>, mut states: Query<&mut ChatState>, mut messages: EventReader, mut instances: Query<&mut Instance>, @@ -469,7 +443,16 @@ fn handle_message_events( for message in messages.iter() { let Ok((username, settings, mut client)) = clients.get_mut(message.client) else { - warn!("Unable to find client for message '{:?}'", message); + warn!("Unable to find client in player list for message '{:?}'", message); + continue; + }; + + let Ok(chat_session) = sessions.get_component::(message.client) else { + warn!("Player `{}` doesn't have a chat session", username.0); + commands.add(DisconnectClient { + client: message.client, + reason: Text::translate(CHAT_DISABLED_MISSING_PROFILE_KEY, []) + }); continue; }; @@ -526,17 +509,13 @@ fn handle_message_events( continue; }; - let Some(session) = &state.session else { - warn!("Player `{}` doesn't have a chat session", username.0); - commands.add(DisconnectClient { - client: message.client, - reason: Text::translate(CHAT_DISABLED_MISSING_PROFILE_KEY, []) - }); - continue; - }; - // Verify that the player's session has not expired - if session.is_expired() { + if SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .expect("Unable to get Unix time") + .as_millis() + >= chat_session.session_data.expires_at as u128 + { warn!("Player `{}` has an expired chat session", username.0); commands.add(DisconnectClient { client: message.client, @@ -575,7 +554,7 @@ fn handle_message_events( // Verify the chat message using the player's session public key and hashed data // against the message signature - if session + if chat_session .public_key .verify( PaddingScheme::new_pkcs1v15_sign::(), From bc0fd647cbee7ad01524b893fe91fe166d6d0181 Mon Sep 17 00:00:00 2001 From: Guac <60993440+guac42@users.noreply.github.com> Date: Mon, 10 Apr 2023 16:38:45 -0700 Subject: [PATCH 12/32] Fix conflict in chat example --- crates/valence/examples/chat.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/valence/examples/chat.rs b/crates/valence/examples/chat.rs index 9e3911b05..13d5f37c7 100644 --- a/crates/valence/examples/chat.rs +++ b/crates/valence/examples/chat.rs @@ -22,7 +22,6 @@ pub fn main() { .add_system(init_clients) .add_system(despawn_disconnected_clients) .add_system(handle_command_events.in_schedule(EventLoopSchedule)) - .add_systems(PlayerList::default_systems()) .run(); } From 82ade5fb87838d37a69fd12efb9e2812adb71eba Mon Sep 17 00:00:00 2001 From: Guac <60993440+guac42@users.noreply.github.com> Date: Mon, 10 Apr 2023 21:46:05 -0700 Subject: [PATCH 13/32] Cleanup --- crates/valence/examples/chat.rs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/crates/valence/examples/chat.rs b/crates/valence/examples/chat.rs index 13d5f37c7..94b9619f7 100644 --- a/crates/valence/examples/chat.rs +++ b/crates/valence/examples/chat.rs @@ -1,8 +1,7 @@ #![allow(clippy::type_complexity)] -use tracing::{warn, Level}; +use tracing::warn; use valence::client::despawn_disconnected_clients; -// TODO: Add CommandExecution event use valence::client::misc::CommandExecution; use valence::entity::player::PlayerEntityBundle; use valence::prelude::*; @@ -11,9 +10,7 @@ use valence::secure_chat::SecureChatPlugin; const SPAWN_Y: i32 = 64; pub fn main() { - tracing_subscriber::fmt() - .with_max_level(Level::DEBUG) - .init(); + tracing_subscriber::fmt().init(); App::new() .add_plugin(ServerPlugin::new(())) From 15f7d0670137f15bc3f0ac7d1ec08005b088af77 Mon Sep 17 00:00:00 2001 From: Guac <60993440+guac42@users.noreply.github.com> Date: Tue, 11 Apr 2023 10:49:23 -0700 Subject: [PATCH 14/32] More cleanup --- crates/valence/src/secure_chat.rs | 81 ++++++++++++++++--------------- 1 file changed, 41 insertions(+), 40 deletions(-) diff --git a/crates/valence/src/secure_chat.rs b/crates/valence/src/secure_chat.rs index 89923be89..f4f394bfe 100644 --- a/crates/valence/src/secure_chat.rs +++ b/crates/valence/src/secure_chat.rs @@ -1,10 +1,11 @@ -use std::collections::{HashMap, HashSet, VecDeque}; +use std::collections::VecDeque; use std::time::SystemTime; use bevy_app::prelude::*; use bevy_ecs::prelude::*; use rsa::pkcs8::DecodePublicKey; use rsa::{PaddingScheme, PublicKey, RsaPublicKey}; +use rustc_hash::{FxHashMap, FxHashSet}; use sha1::{Digest, Sha1}; use sha2::Sha256; use tracing::{debug, info, warn}; @@ -21,7 +22,6 @@ use valence_protocol::translation_key::{ MULTIPLAYER_DISCONNECT_TOO_MANY_PENDING_CHATS, MULTIPLAYER_DISCONNECT_UNSIGNED_CHAT, }; use valence_protocol::types::MessageSignature; -use valence_protocol::var_int::VarInt; use crate::client::misc::{ChatMessage, MessageAcknowledgment, PlayerSession}; use crate::client::settings::ClientSettings; @@ -66,8 +66,11 @@ impl Default for ChatState { } impl ChatState { - pub fn add_pending(&mut self, last_seen: &mut VecDeque<[u8; 256]>, signature: &[u8; 256]) { - self.signature_storage.add(last_seen, signature); + /// Updates the chat state's previously seen signatures with a new one + /// `signature`. + pub fn add_pending(&mut self, last_seen: &VecDeque<[u8; 256]>, signature: &[u8; 256]) { + self.signature_storage + .add(&mut last_seen.clone(), signature); self.validator.add_pending(signature); } } @@ -88,6 +91,7 @@ impl AcknowledgementValidator { /// Add a message pending acknowledgement via its `signature`. pub fn add_pending(&mut self, signature: &[u8; 256]) { + // Attempting to add the last signature again. if matches!(&self.last_signature, Some(last_sig) if signature == last_sig.as_ref()) { return; } @@ -104,12 +108,13 @@ impl AcknowledgementValidator { /// validator with at least 20 messages. Returns `true` if messages are /// removed and `false` if they are not. pub fn remove_until(&mut self, index: i32) -> bool { - // Ensure that there will still be 20 messages in the array + // Ensure that there will still be 20 messages in the array. if index >= 0 && index <= (self.messages.len() - 20) as i32 { self.messages.drain(0..index as usize); - if self.messages.len() < 20 { - warn!("Message validator 'messages' shrunk!"); - } + debug_assert!( + self.messages.len() >= 20, + "Message validator 'messages' shrunk!" + ); return true; } false @@ -205,7 +210,7 @@ impl MessageChain { None => self.link, Some(current) => { let temp = *current; - current.index += 1; + current.index = current.index.wrapping_add(1); Some(temp) } } @@ -230,14 +235,14 @@ impl MessageLink { #[derive(Clone, Debug)] struct MessageSignatureStorage { signatures: [Option<[u8; 256]>; 128], - indices: HashMap<[u8; 256], i32>, + indices: FxHashMap<[u8; 256], i32>, } impl Default for MessageSignatureStorage { fn default() -> Self { Self { signatures: [None; 128], - indices: HashMap::new(), + indices: FxHashMap::default(), } } } @@ -258,7 +263,7 @@ impl MessageSignatureStorage { /// Warning: this consumes `last_seen`. pub fn add(&mut self, last_seen: &mut VecDeque<[u8; 256]>, signature: &[u8; 256]) { last_seen.push_back(*signature); - let mut sig_set = HashSet::new(); + let mut sig_set = FxHashSet::default(); for sig in last_seen.iter() { sig_set.insert(*sig); } @@ -327,7 +332,7 @@ fn handle_session_events( continue; }; - // Verify that the session key has not expired + // Verify that the session key has not expired. if SystemTime::now() .duration_since(SystemTime::UNIX_EPOCH) .expect("Unable to get Unix time") @@ -342,19 +347,19 @@ fn handle_session_events( continue; } - // Serialize the session data + // Serialize the session data. let mut serialized = Vec::with_capacity(318); serialized.extend_from_slice(uuid.0.into_bytes().as_slice()); serialized.extend_from_slice(session.session_data.expires_at.to_be_bytes().as_ref()); serialized.extend_from_slice(session.session_data.public_key_data.as_ref()); - // Hash the session data using the SHA-1 algorithm + // Hash the session data using the SHA-1 algorithm. let mut hasher = Sha1::new(); hasher.update(&serialized); let hash = hasher.finalize(); // Verify the session data using Mojang's public key and the hashed session data - // against the message signature + // against the message signature. if services_state .public_key .verify( @@ -371,11 +376,11 @@ fn handle_session_events( }); } - // Decode the player's session public key from the data + // Decode the player's session public key from the data. if let Ok(public_key) = RsaPublicKey::from_public_key_der(session.session_data.public_key_data.as_ref()) { - // Update the player's chat state data with the new player session data + // Update the player's chat state data with the new player session data. state.chain.link = Some(MessageLink { index: 0, sender: uuid.0, @@ -391,7 +396,7 @@ fn handle_session_events( } else { // This shouldn't happen considering that it is highly unlikely that Mojang // would provide the client with a malformed key. By this point the - // key signature has been verified + // key signature has been verified. warn!("Received malformed profile key data from '{}'", username.0); commands.add(DisconnectClient { client: session.client, @@ -434,7 +439,7 @@ fn handle_message_acknowledgement( fn handle_message_events( mut clients: Query<(&Username, &ClientSettings, &mut Client), With>, sessions: Query<&ChatSession, With>, - mut states: Query<&mut ChatState>, + mut states: Query<&mut ChatState, With>, mut messages: EventReader, mut instances: Query<&mut Instance>, mut commands: Commands, @@ -461,13 +466,13 @@ fn handle_message_events( continue; }; - // Ensure that the client isn't sending messages while their chat is hidden + // Ensure that the client isn't sending messages while their chat is hidden. if settings.chat_mode == ChatMode::Hidden { client.send_message(Text::translate(CHAT_DISABLED_OPTIONS, []).color(Color::RED)); continue; } - // Ensure we are receiving chat messages in order + // Ensure we are receiving chat messages in order. if message.timestamp < state.last_message_timestamp { warn!( "{:?} sent out-of-order chat: '{:?}'", @@ -483,7 +488,7 @@ fn handle_message_events( state.last_message_timestamp = message.timestamp; - // Validate the message acknowledgements + // Validate the message acknowledgements. match state .validator .validate(&message.acknowledgements, message.message_index) @@ -496,11 +501,7 @@ fn handle_message_events( }); continue; } - Some(mut last_seen) => { - // This whole process should probably be done on another thread similarly to - // chunk loading in the 'terrain.rs' example, as this is what - // the notchian server does - + Some(last_seen) => { let Some(link) = &state.chain.next_link() else { client.send_message(Text::translate( CHAT_DISABLED_CHAIN_BROKEN, @@ -509,7 +510,7 @@ fn handle_message_events( continue; }; - // Verify that the player's session has not expired + // Verify that the player's session has not expired. if SystemTime::now() .duration_since(SystemTime::UNIX_EPOCH) .expect("Unable to get Unix time") @@ -524,7 +525,7 @@ fn handle_message_events( continue; } - // Verify that the chat message is signed + // Verify that the chat message is signed. let Some(message_signature) = &message.signature else { warn!("Received unsigned chat message from `{}`", username.0); commands.add(DisconnectClient { @@ -534,13 +535,13 @@ fn handle_message_events( continue; }; - // Create the hash digest used to verify the chat message + // Create the hash digest used to verify the chat message. let mut hasher = Sha256::new_with_prefix([0u8, 0, 0, 1]); - // Update the hash with the player's message chain state + // Update the hash with the player's message chain state. link.update_hash(&mut hasher); - // Update the hash with the message contents + // Update the hash with the message contents. hasher.update(message.salt.to_be_bytes()); hasher.update((message.timestamp / 1000).to_be_bytes()); let bytes = message.message.as_bytes(); @@ -553,7 +554,7 @@ fn handle_message_events( let hashed = hasher.finalize(); // Verify the chat message using the player's session public key and hashed data - // against the message signature + // against the message signature. if chat_session .public_key .verify( @@ -571,7 +572,7 @@ fn handle_message_events( continue; } - // Create a list of messages that have been seen by the client + // Create a list of messages that have been seen by the client. let previous = last_seen .iter() .map(|sig| match state.signature_storage.index_of(sig) { @@ -584,7 +585,7 @@ fn handle_message_events( instance.write_packet(&ChatMessageS2c { sender: link.sender, - index: VarInt(link.index), + index: link.index.into(), message_signature: Some(message_signature.as_ref()), message: message.message.as_ref(), time_stamp: message.timestamp, @@ -592,15 +593,15 @@ fn handle_message_events( previous_messages: previous, unsigned_content: None, filter_type: MessageFilterType::PassThrough, - chat_type: VarInt(0), + chat_type: 0.into(), network_name: Text::from(username.0.clone()).into(), network_target_name: None, }); - // Update the other clients' chat states + // Update the other clients' chat states. for mut state in states.iter_mut() { - // Add pending acknowledgement - state.add_pending(&mut last_seen, message_signature.as_ref()); + // Add pending acknowledgement. + state.add_pending(&last_seen, message_signature.as_ref()); if state.validator.message_count() > 4096 { commands.add(DisconnectClient { client: message.client, From a6d6889fc8f74a3b882f38671ea7010f7003536e Mon Sep 17 00:00:00 2001 From: Guac Date: Mon, 1 May 2023 22:39:37 -0700 Subject: [PATCH 15/32] Fixed bug The player public session data wasn't being sent for existing clients to new clients. --- crates/valence/src/player_list.rs | 17 ++- crates/valence/src/secure_chat.rs | 246 +++++++++++++++--------------- 2 files changed, 134 insertions(+), 129 deletions(-) diff --git a/crates/valence/src/player_list.rs b/crates/valence/src/player_list.rs index c8901954c..c2ee3d64f 100644 --- a/crates/valence/src/player_list.rs +++ b/crates/valence/src/player_list.rs @@ -181,6 +181,7 @@ fn init_player_list_for_clients( &Ping, &DisplayName, &Listed, + Option<&ChatSession>, ), With, >, @@ -192,16 +193,26 @@ fn init_player_list_for_clients( .with_update_game_mode(true) .with_update_listed(true) .with_update_latency(true) - .with_update_display_name(true); + .with_update_display_name(true) + .with_initialize_chat(true); let entries: Vec<_> = entries .iter() .map( - |(uuid, username, props, game_mode, ping, display_name, listed)| Entry { + |( + uuid, + username, + props, + game_mode, + ping, + display_name, + listed, + chat_session, + )| Entry { player_uuid: uuid.0, username: &username.0, properties: Cow::Borrowed(&props.0), - chat_data: None, + chat_data: chat_session.map(|s| s.session_data.clone().into()), listed: listed.0, ping: ping.0, game_mode: (*game_mode).into(), diff --git a/crates/valence/src/secure_chat.rs b/crates/valence/src/secure_chat.rs index f4f394bfe..edbd132e1 100644 --- a/crates/valence/src/secure_chat.rs +++ b/crates/valence/src/secure_chat.rs @@ -27,7 +27,7 @@ use crate::client::misc::{ChatMessage, MessageAcknowledgment, PlayerSession}; use crate::client::settings::ClientSettings; use crate::client::{Client, DisconnectClient, FlushPacketsSet}; use crate::component::{UniqueId, Username}; -use crate::instance::Instance; +use crate::packet::WritePacket; use crate::player_list::{ChatSession, PlayerListEntry}; const MOJANG_KEY_DATA: &[u8] = include_bytes!("../../../assets/yggdrasil_session_pubkey.der"); @@ -68,10 +68,11 @@ impl Default for ChatState { impl ChatState { /// Updates the chat state's previously seen signatures with a new one /// `signature`. - pub fn add_pending(&mut self, last_seen: &VecDeque<[u8; 256]>, signature: &[u8; 256]) { + /// Warning this modifies `last_seen`. + pub fn add_pending(&mut self, last_seen: &mut VecDeque<[u8; 256]>, signature: [u8; 256]) { self.signature_storage - .add(&mut last_seen.clone(), signature); - self.validator.add_pending(signature); + .add(last_seen, &signature); + self.validator.add_pending(&signature); } } @@ -437,18 +438,17 @@ fn handle_message_acknowledgement( } fn handle_message_events( - mut clients: Query<(&Username, &ClientSettings, &mut Client), With>, + mut clients: Query< + (&mut Client, &mut ChatState, &Username, &ClientSettings), + With, + >, sessions: Query<&ChatSession, With>, - mut states: Query<&mut ChatState, With>, mut messages: EventReader, - mut instances: Query<&mut Instance>, mut commands: Commands, ) { - let mut instance = instances.single_mut(); - for message in messages.iter() { - let Ok((username, settings, mut client)) = clients.get_mut(message.client) else { - warn!("Unable to find client in player list for message '{:?}'", message); + let Ok((mut client, mut state, username, settings)) = clients.get_mut(message.client) else { + warn!("Unable to find client for message '{:?}'", message); continue; }; @@ -461,11 +461,6 @@ fn handle_message_events( continue; }; - let Ok(mut state) = states.get_component_mut::(message.client) else { - warn!("Unable to find chat state for client '{:?}'", username.0); - continue; - }; - // Ensure that the client isn't sending messages while their chat is hidden. if settings.chat_mode == ChatMode::Hidden { client.send_message(Text::translate(CHAT_DISABLED_OPTIONS, []).color(Color::RED)); @@ -489,7 +484,7 @@ fn handle_message_events( state.last_message_timestamp = message.timestamp; // Validate the message acknowledgements. - match state + let last_seen = match state .validator .validate(&message.acknowledgements, message.message_index) { @@ -501,118 +496,117 @@ fn handle_message_events( }); continue; } - Some(last_seen) => { - let Some(link) = &state.chain.next_link() else { - client.send_message(Text::translate( - CHAT_DISABLED_CHAIN_BROKEN, - [], - ).color(Color::RED)); - continue; - }; - - // Verify that the player's session has not expired. - if SystemTime::now() - .duration_since(SystemTime::UNIX_EPOCH) - .expect("Unable to get Unix time") - .as_millis() - >= chat_session.session_data.expires_at as u128 - { - warn!("Player `{}` has an expired chat session", username.0); - commands.add(DisconnectClient { - client: message.client, - reason: Text::translate(CHAT_DISABLED_EXPIRED_PROFILE_KEY, []), - }); - continue; - } + Some(last_seen) => last_seen, + }; - // Verify that the chat message is signed. - let Some(message_signature) = &message.signature else { - warn!("Received unsigned chat message from `{}`", username.0); - commands.add(DisconnectClient { - client: message.client, - reason: Text::translate(MULTIPLAYER_DISCONNECT_UNSIGNED_CHAT, []) - }); - continue; - }; - - // Create the hash digest used to verify the chat message. - let mut hasher = Sha256::new_with_prefix([0u8, 0, 0, 1]); - - // Update the hash with the player's message chain state. - link.update_hash(&mut hasher); - - // Update the hash with the message contents. - hasher.update(message.salt.to_be_bytes()); - hasher.update((message.timestamp / 1000).to_be_bytes()); - let bytes = message.message.as_bytes(); - hasher.update((bytes.len() as u32).to_be_bytes()); - hasher.update(bytes); - hasher.update((last_seen.len() as u32).to_be_bytes()); - for sig in last_seen.iter() { - hasher.update(sig); - } - let hashed = hasher.finalize(); - - // Verify the chat message using the player's session public key and hashed data - // against the message signature. - if chat_session - .public_key - .verify( - PaddingScheme::new_pkcs1v15_sign::(), - &hashed, - message_signature.as_ref(), - ) - .is_err() - { - warn!("Failed to verify chat message from `{}`", username.0); - commands.add(DisconnectClient { - client: message.client, - reason: Text::translate(MULTIPLAYER_DISCONNECT_UNSIGNED_CHAT, []), - }); - continue; - } + let Some(link) = &state.chain.next_link() else { + client.send_message(Text::translate( + CHAT_DISABLED_CHAIN_BROKEN, + [], + ).color(Color::RED)); + continue; + }; - // Create a list of messages that have been seen by the client. - let previous = last_seen - .iter() - .map(|sig| match state.signature_storage.index_of(sig) { - Some(index) => MessageSignature::ByIndex(index), - None => MessageSignature::BySignature(sig), - }) - .collect::>(); - - info!("{}: {}", username.0, message.message.as_ref()); - - instance.write_packet(&ChatMessageS2c { - sender: link.sender, - index: link.index.into(), - message_signature: Some(message_signature.as_ref()), - message: message.message.as_ref(), - time_stamp: message.timestamp, - salt: message.salt, - previous_messages: previous, - unsigned_content: None, - filter_type: MessageFilterType::PassThrough, - chat_type: 0.into(), - network_name: Text::from(username.0.clone()).into(), - network_target_name: None, - }); + // Verify that the player's session has not expired. + if SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .expect("Unable to get Unix time") + .as_millis() + >= chat_session.session_data.expires_at as u128 + { + warn!("Player `{}` has an expired chat session", username.0); + commands.add(DisconnectClient { + client: message.client, + reason: Text::translate(CHAT_DISABLED_EXPIRED_PROFILE_KEY, []), + }); + continue; + } - // Update the other clients' chat states. - for mut state in states.iter_mut() { - // Add pending acknowledgement. - state.add_pending(&last_seen, message_signature.as_ref()); - if state.validator.message_count() > 4096 { - commands.add(DisconnectClient { - client: message.client, - reason: Text::translate( - MULTIPLAYER_DISCONNECT_TOO_MANY_PENDING_CHATS, - [], - ), - }); - continue; - } - } + // Verify that the chat message is signed. + let Some(message_signature) = &message.signature else { + warn!("Received unsigned chat message from `{}`", username.0); + commands.add(DisconnectClient { + client: message.client, + reason: Text::translate(MULTIPLAYER_DISCONNECT_UNSIGNED_CHAT, []) + }); + continue; + }; + + // Create the hash digest used to verify the chat message. + let mut hasher = Sha256::new_with_prefix([0u8, 0, 0, 1]); + + // Update the hash with the player's message chain state. + link.update_hash(&mut hasher); + + // Update the hash with the message contents. + hasher.update(message.salt.to_be_bytes()); + hasher.update((message.timestamp / 1000).to_be_bytes()); + let bytes = message.message.as_bytes(); + hasher.update((bytes.len() as u32).to_be_bytes()); + hasher.update(bytes); + hasher.update((last_seen.len() as u32).to_be_bytes()); + for sig in last_seen.iter() { + hasher.update(sig); + } + let hashed = hasher.finalize(); + + // Verify the chat message using the player's session public key and hashed data + // against the message signature. + if chat_session + .public_key + .verify( + PaddingScheme::new_pkcs1v15_sign::(), + &hashed, + message_signature.as_ref(), + ) + .is_err() + { + warn!("Failed to verify chat message from `{}`", username.0); + commands.add(DisconnectClient { + client: message.client, + reason: Text::translate(MULTIPLAYER_DISCONNECT_UNSIGNED_CHAT, []), + }); + continue; + } + + info!("{}: {}", username.0, message.message.as_ref()); + + let username = username.0.clone(); + + // Broadcast the chat message to other clients. + for (mut client, mut state, ..) in clients.iter_mut() { + // Create a list of messages that have been seen by the client. + let previous = last_seen + .iter() + .map(|sig| match state.signature_storage.index_of(sig) { + Some(index) => MessageSignature::ByIndex(index), + None => MessageSignature::BySignature(sig), + }) + .collect::>(); + + client.write_packet(&ChatMessageS2c { + sender: link.sender, + index: link.index.into(), + message_signature: Some(message_signature.as_ref()), + message: message.message.as_ref(), + time_stamp: message.timestamp, + salt: message.salt, + previous_messages: previous, + unsigned_content: None, + filter_type: MessageFilterType::PassThrough, + chat_type: 0.into(), + network_name: Text::from(username.clone()).into(), + network_target_name: None, + }); + // Add pending acknowledgement. + state.add_pending(&mut last_seen.clone(), *message_signature.as_ref()); + if state.validator.message_count() > 4096 { + warn!("User has too many pending chats `{}`", username); + commands.add(DisconnectClient { + client: message.client, + reason: Text::translate(MULTIPLAYER_DISCONNECT_TOO_MANY_PENDING_CHATS, []), + }); + continue; } } } From dfc5278065189558003eb846c5494d87904d6f36 Mon Sep 17 00:00:00 2001 From: Guac Date: Tue, 2 May 2023 23:21:58 -0700 Subject: [PATCH 16/32] Fix merge conflicts --- Cargo.toml | 1 + crates/valence/Cargo.toml | 2 +- crates/valence/examples/chat.rs | 12 +-- crates/valence/src/lib.rs | 16 ++-- crates/valence_chat/Cargo.toml | 21 +++++ crates/valence_chat/README.md | 10 +++ .../src/chat_type.rs | 9 +- .../src/lib.rs} | 85 ++++++++++++------- crates/valence_client/src/misc.rs | 8 +- .../src/packet/message_signature.rs | 32 +++---- .../src/packet/s2c/play/player_list.rs | 2 +- crates/valence_player_list/Cargo.toml | 1 + crates/valence_player_list/src/lib.rs | 1 + 13 files changed, 125 insertions(+), 75 deletions(-) create mode 100644 crates/valence_chat/Cargo.toml create mode 100644 crates/valence_chat/README.md rename crates/{valence => valence_chat}/src/chat_type.rs (98%) rename crates/{valence/src/secure_chat.rs => valence_chat/src/lib.rs} (90%) diff --git a/Cargo.toml b/Cargo.toml index 9f0ba57fb..8f72aa1c7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -75,6 +75,7 @@ valence_anvil.path = "crates/valence_anvil" valence_biome.path = "crates/valence_biome" valence_block.path = "crates/valence_block" valence_build_utils.path = "crates/valence_build_utils" +valence_chat.path = "crates/valence_chat" valence_client.path = "crates/valence_client" valence_core_macros.path = "crates/valence_core_macros" valence_core.path = "crates/valence_core" diff --git a/crates/valence/Cargo.toml b/crates/valence/Cargo.toml index 631685d88..f6e483bae 100644 --- a/crates/valence/Cargo.toml +++ b/crates/valence/Cargo.toml @@ -11,7 +11,7 @@ keywords = ["minecraft", "gamedev", "server", "ecs"] categories = ["game-engines"] [features] -default = ["network", "player_list", "inventory", "anvil"] +default = ["network", "player_list", "inventory", "anvil", "chat"] network = ["dep:valence_network"] player_list = ["dep:valence_player_list"] inventory = ["dep:valence_inventory"] diff --git a/crates/valence/examples/chat.rs b/crates/valence/examples/chat.rs index 94b9619f7..6f7442572 100644 --- a/crates/valence/examples/chat.rs +++ b/crates/valence/examples/chat.rs @@ -5,7 +5,6 @@ use valence::client::despawn_disconnected_clients; use valence::client::misc::CommandExecution; use valence::entity::player::PlayerEntityBundle; use valence::prelude::*; -use valence::secure_chat::SecureChatPlugin; const SPAWN_Y: i32 = 64; @@ -13,12 +12,13 @@ pub fn main() { tracing_subscriber::fmt().init(); App::new() - .add_plugin(ServerPlugin::new(())) - .add_plugin(SecureChatPlugin) + .add_plugins(DefaultPlugins) .add_startup_system(setup) - .add_system(init_clients) - .add_system(despawn_disconnected_clients) - .add_system(handle_command_events.in_schedule(EventLoopSchedule)) + .add_systems(( + init_clients, + despawn_disconnected_clients, + handle_command_events.in_schedule(EventLoopSchedule), + )) .run(); } diff --git a/crates/valence/src/lib.rs b/crates/valence/src/lib.rs index adc6e7a32..b375f9313 100644 --- a/crates/valence/src/lib.rs +++ b/crates/valence/src/lib.rs @@ -28,6 +28,8 @@ mod tests; #[cfg(feature = "anvil")] pub use valence_anvil as anvil; +#[cfg(feature = "chat")] +pub use valence_chat as secure_chat; pub use valence_core::*; #[cfg(feature = "inventory")] pub use valence_inventory as inventory; @@ -35,8 +37,6 @@ pub use valence_inventory as inventory; pub use valence_network as network; #[cfg(feature = "player_list")] pub use valence_player_list as player_list; -#[cfg(feature = "chat")] -pub use valence_chat as chat; pub use { bevy_app as app, bevy_ecs as ecs, glam, valence_biome as biome, valence_block as block, valence_client as client, valence_dimension as dimension, valence_entity as entity, @@ -99,7 +99,7 @@ pub mod prelude { #[cfg(feature = "player_list")] pub use player_list::{PlayerList, PlayerListEntry}; #[cfg(feature = "chat")] - pub use chat::{ChatType, ChatTypeRegistry}; + pub use secure_chat::chat_type::{ChatType, ChatTypeRegistry}; pub use text::{Color, Text, TextFormat}; pub use valence_core::ident; // Export the `ident!` macro. pub use valence_core::uuid::UniqueId; @@ -144,16 +144,14 @@ impl PluginGroup for DefaultPlugins { group = group.add(valence_inventory::InventoryPlugin); } - #[cfg(feature = "chat")] + #[cfg(feature = "anvil")] { - group = group - .add(valence_chat::SecureChatPlugin) - .add(valence_chat::chat_type::ChatTypePlugin); + // No plugin... yet. } - #[cfg(feature = "anvil")] + #[cfg(feature = "chat")] { - // No plugin... yet. + group = group.add(valence_chat::SecureChatPlugin); } group diff --git a/crates/valence_chat/Cargo.toml b/crates/valence_chat/Cargo.toml new file mode 100644 index 000000000..2ea10e7f7 --- /dev/null +++ b/crates/valence_chat/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "valence_chat" +version.workspace = true +edition.workspace = true + +[dependencies] +anyhow.workspace = true +bevy_app.workspace = true +bevy_ecs.workspace = true +rsa.workspace = true +rustc-hash.workspace = true +sha1 = { workspace = true, features = ["oid"] } +sha2 = { workspace = true, features = ["oid"] } +tracing.workspace = true +valence_core.workspace = true +valence_client.workspace = true +valence_instance.workspace = true +valence_nbt.workspace = true +valence_player_list.workspace = true +valence_registry.workspace = true +uuid.workspace = true \ No newline at end of file diff --git a/crates/valence_chat/README.md b/crates/valence_chat/README.md new file mode 100644 index 000000000..b26f0bb3e --- /dev/null +++ b/crates/valence_chat/README.md @@ -0,0 +1,10 @@ +# valence_chat + +Provides support for cryptographically verified chat messaging on the server. + +This crate contains the secure chat plugin as well as chat types and the chat type registry. Minecraft's default chat types are added to the registry by default. Chat types contain information about how chat is styled, such as the chat color. + +### **NOTE:** +- Modifying the chat type registry after the server has started can +break invariants within instances and clients! Make sure there are no +instances or clients spawned before mutating. \ No newline at end of file diff --git a/crates/valence/src/chat_type.rs b/crates/valence_chat/src/chat_type.rs similarity index 98% rename from crates/valence/src/chat_type.rs rename to crates/valence_chat/src/chat_type.rs index 54213efee..2222cd7e5 100644 --- a/crates/valence/src/chat_type.rs +++ b/crates/valence_chat/src/chat_type.rs @@ -12,12 +12,11 @@ use anyhow::{bail, Context}; use bevy_app::{CoreSet, Plugin, StartupSet}; use bevy_ecs::prelude::*; use tracing::error; +use valence_core::ident; +use valence_core::ident::Ident; +use valence_core::text::Color; use valence_nbt::{compound, Compound, List, Value}; -use valence_protocol::ident; -use valence_protocol::ident::Ident; -use valence_protocol::text::Color; - -use crate::registry_codec::{RegistryCodec, RegistryCodecSet, RegistryValue}; +use valence_registry::{RegistryCodec, RegistryCodecSet, RegistryValue}; #[derive(Resource)] pub struct ChatTypeRegistry { diff --git a/crates/valence/src/secure_chat.rs b/crates/valence_chat/src/lib.rs similarity index 90% rename from crates/valence/src/secure_chat.rs rename to crates/valence_chat/src/lib.rs index edbd132e1..0acb64ea2 100644 --- a/crates/valence/src/secure_chat.rs +++ b/crates/valence_chat/src/lib.rs @@ -1,8 +1,30 @@ +#![doc = include_str!("../README.md")] +#![deny( + rustdoc::broken_intra_doc_links, + rustdoc::private_intra_doc_links, + rustdoc::missing_crate_level_docs, + rustdoc::invalid_codeblock_attributes, + rustdoc::invalid_rust_codeblocks, + rustdoc::bare_urls, + rustdoc::invalid_html_tags +)] +#![warn( + trivial_casts, + trivial_numeric_casts, + unused_lifetimes, + unused_import_braces, + unreachable_pub, + clippy::dbg_macro +)] + +pub mod chat_type; + use std::collections::VecDeque; use std::time::SystemTime; use bevy_app::prelude::*; use bevy_ecs::prelude::*; +use chat_type::ChatTypePlugin; use rsa::pkcs8::DecodePublicKey; use rsa::{PaddingScheme, PublicKey, RsaPublicKey}; use rustc_hash::{FxHashMap, FxHashSet}; @@ -10,25 +32,24 @@ use sha1::{Digest, Sha1}; use sha2::Sha256; use tracing::{debug, info, warn}; use uuid::Uuid; -use valence_protocol::packet::c2s::play::client_settings::ChatMode; -use valence_protocol::packet::s2c::play::chat_message::MessageFilterType; -use valence_protocol::packet::s2c::play::ChatMessageS2c; -use valence_protocol::text::{Color, Text, TextFormat}; -use valence_protocol::translation_key::{ +use valence_client::misc::{ChatMessage, MessageAcknowledgment, PlayerSession}; +use valence_client::settings::ClientSettings; +use valence_client::{Client, DisconnectClient, FlushPacketsSet, Username}; +use valence_core::packet::c2s::play::client_settings::ChatMode; +use valence_core::packet::encode::WritePacket; +use valence_core::packet::message_signature::MessageSignature; +use valence_core::packet::s2c::play::chat_message::MessageFilterType; +use valence_core::packet::s2c::play::ChatMessageS2c; +use valence_core::text::{Color, Text, TextFormat}; +use valence_core::translation_key::{ CHAT_DISABLED_CHAIN_BROKEN, CHAT_DISABLED_EXPIRED_PROFILE_KEY, CHAT_DISABLED_MISSING_PROFILE_KEY, CHAT_DISABLED_OPTIONS, MULTIPLAYER_DISCONNECT_CHAT_VALIDATION_FAILED, MULTIPLAYER_DISCONNECT_EXPIRED_PUBLIC_KEY, MULTIPLAYER_DISCONNECT_INVALID_PUBLIC_KEY_SIGNATURE, MULTIPLAYER_DISCONNECT_OUT_OF_ORDER_CHAT, MULTIPLAYER_DISCONNECT_TOO_MANY_PENDING_CHATS, MULTIPLAYER_DISCONNECT_UNSIGNED_CHAT, }; -use valence_protocol::types::MessageSignature; - -use crate::client::misc::{ChatMessage, MessageAcknowledgment, PlayerSession}; -use crate::client::settings::ClientSettings; -use crate::client::{Client, DisconnectClient, FlushPacketsSet}; -use crate::component::{UniqueId, Username}; -use crate::packet::WritePacket; -use crate::player_list::{ChatSession, PlayerListEntry}; +use valence_core::uuid::UniqueId; +use valence_player_list::{ChatSession, PlayerListEntry}; const MOJANG_KEY_DATA: &[u8] = include_bytes!("../../../assets/yggdrasil_session_pubkey.der"); @@ -38,7 +59,7 @@ struct MojangServicesState { } impl MojangServicesState { - pub fn new(public_key: RsaPublicKey) -> Self { + fn new(public_key: RsaPublicKey) -> Self { Self { public_key } } } @@ -69,9 +90,8 @@ impl ChatState { /// Updates the chat state's previously seen signatures with a new one /// `signature`. /// Warning this modifies `last_seen`. - pub fn add_pending(&mut self, last_seen: &mut VecDeque<[u8; 256]>, signature: [u8; 256]) { - self.signature_storage - .add(last_seen, &signature); + fn add_pending(&mut self, last_seen: &mut VecDeque<[u8; 256]>, signature: [u8; 256]) { + self.signature_storage.add(last_seen, &signature); self.validator.add_pending(&signature); } } @@ -83,7 +103,7 @@ struct AcknowledgementValidator { } impl AcknowledgementValidator { - pub fn new() -> Self { + fn new() -> Self { Self { messages: vec![None; 20], last_signature: None, @@ -91,7 +111,7 @@ impl AcknowledgementValidator { } /// Add a message pending acknowledgement via its `signature`. - pub fn add_pending(&mut self, signature: &[u8; 256]) { + fn add_pending(&mut self, signature: &[u8; 256]) { // Attempting to add the last signature again. if matches!(&self.last_signature, Some(last_sig) if signature == last_sig.as_ref()) { return; @@ -108,7 +128,7 @@ impl AcknowledgementValidator { /// Message signatures will only be removed if the result leaves the /// validator with at least 20 messages. Returns `true` if messages are /// removed and `false` if they are not. - pub fn remove_until(&mut self, index: i32) -> bool { + fn remove_until(&mut self, index: i32) -> bool { // Ensure that there will still be 20 messages in the array. if index >= 0 && index <= (self.messages.len() - 20) as i32 { self.messages.drain(0..index as usize); @@ -125,7 +145,7 @@ impl AcknowledgementValidator { /// /// Returns a [`VecDeque`] of acknowledged message signatures if the /// `acknowledgements` are valid and `None` if they are invalid. - pub fn validate( + fn validate( &mut self, acknowledgements: &[u8; 3], message_index: i32, @@ -185,15 +205,15 @@ impl AcknowledgementValidator { } /// The number of pending messages in the validator. - pub fn message_count(&self) -> usize { + fn message_count(&self) -> usize { self.messages.len() } } #[derive(Clone, Debug)] struct AcknowledgedMessage { - pub signature: [u8; 256], - pub pending: bool, + signature: [u8; 256], + pending: bool, } #[derive(Clone, Default, Debug)] @@ -202,11 +222,11 @@ struct MessageChain { } impl MessageChain { - pub fn new() -> Self { + fn new() -> Self { Self::default() } - pub fn next_link(&mut self) -> Option { + fn next_link(&mut self) -> Option { match &mut self.link { None => self.link, Some(current) => { @@ -226,7 +246,7 @@ struct MessageLink { } impl MessageLink { - pub fn update_hash(&self, hasher: &mut impl Digest) { + fn update_hash(&self, hasher: &mut impl Digest) { hasher.update(self.sender.into_bytes()); hasher.update(self.session_id.into_bytes()); hasher.update(self.index.to_be_bytes()); @@ -249,12 +269,12 @@ impl Default for MessageSignatureStorage { } impl MessageSignatureStorage { - pub fn new() -> Self { + fn new() -> Self { Self::default() } /// Get the index of the `signature` in the storage if it exists. - pub fn index_of(&self, signature: &[u8; 256]) -> Option { + fn index_of(&self, signature: &[u8; 256]) -> Option { self.indices.get(signature).copied() } @@ -262,7 +282,7 @@ impl MessageSignatureStorage { /// `signature` to the storage. /// /// Warning: this consumes `last_seen`. - pub fn add(&mut self, last_seen: &mut VecDeque<[u8; 256]>, signature: &[u8; 256]) { + fn add(&mut self, last_seen: &mut VecDeque<[u8; 256]>, signature: &[u8; 256]) { last_seen.push_back(*signature); let mut sig_set = FxHashSet::default(); for sig in last_seen.iter() { @@ -297,7 +317,8 @@ impl Plugin for SecureChatPlugin { let mojang_pub_key = RsaPublicKey::from_public_key_der(MOJANG_KEY_DATA) .expect("Error creating Mojang public key"); - app.insert_resource(MojangServicesState::new(mojang_pub_key)) + app.add_plugin(ChatTypePlugin) + .insert_resource(MojangServicesState::new(mojang_pub_key)) .add_systems( ( init_chat_states, @@ -594,7 +615,7 @@ fn handle_message_events( previous_messages: previous, unsigned_content: None, filter_type: MessageFilterType::PassThrough, - chat_type: 0.into(), + chat_type: 0.into(), // TODO: Make chat type for player messages selectable network_name: Text::from(username.clone()).into(), network_target_name: None, }); diff --git a/crates/valence_client/src/misc.rs b/crates/valence_client/src/misc.rs index 3b4cf56da..d499fb465 100644 --- a/crates/valence_client/src/misc.rs +++ b/crates/valence_client/src/misc.rs @@ -1,9 +1,11 @@ use bevy_app::prelude::*; use bevy_ecs::prelude::*; use glam::Vec3; -use valence_protocol::block_pos::BlockPos; -use valence_protocol::packet::c2s::play::player_session::PlayerSessionData; -use valence_protocol::packet::c2s::play::{ +use valence_core::block_pos::BlockPos; +use valence_core::direction::Direction; +use valence_core::hand::Hand; +use valence_core::packet::c2s::play::player_session::PlayerSessionData; +use valence_core::packet::c2s::play::{ ChatMessageC2s, ClientStatusC2s, CommandExecutionC2s, HandSwingC2s, MessageAcknowledgmentC2s, PlayerInteractBlockC2s, PlayerInteractItemC2s, PlayerSessionC2s, ResourcePackStatusC2s, }; diff --git a/crates/valence_core/src/packet/message_signature.rs b/crates/valence_core/src/packet/message_signature.rs index bc78225a1..28f546af4 100644 --- a/crates/valence_core/src/packet/message_signature.rs +++ b/crates/valence_core/src/packet/message_signature.rs @@ -4,18 +4,19 @@ use super::var_int::VarInt; use super::{Decode, Encode}; #[derive(Copy, Clone, PartialEq, Debug)] -pub struct MessageSignature<'a> { - pub message_id: i32, - pub signature: Option<&'a [u8; 256]>, +pub enum MessageSignature<'a> { + ByIndex(i32), + BySignature(&'a [u8; 256]), } impl<'a> Encode for MessageSignature<'a> { fn encode(&self, mut w: impl Write) -> anyhow::Result<()> { - VarInt(self.message_id + 1).encode(&mut w)?; - - match self.signature { - None => {} - Some(signature) => signature.encode(&mut w)?, + match self { + MessageSignature::ByIndex(index) => VarInt(index + 1).encode(&mut w)?, + MessageSignature::BySignature(signature) => { + VarInt(0).encode(&mut w)?; + signature.encode(&mut w)?; + } } Ok(()) @@ -24,17 +25,12 @@ impl<'a> Encode for MessageSignature<'a> { impl<'a> Decode<'a> for MessageSignature<'a> { fn decode(r: &mut &'a [u8]) -> anyhow::Result { - let message_id = VarInt::decode(r)?.0 - 1; // TODO: this can underflow. + let index = VarInt::decode(r)?.0.saturating_sub(1); - let signature = if message_id == -1 { - Some(<&[u8; 256]>::decode(r)?) + if index == -1 { + Ok(MessageSignature::BySignature(<&[u8; 256]>::decode(r)?)) } else { - None - }; - - Ok(Self { - message_id, - signature, - }) + Ok(MessageSignature::ByIndex(index)) + } } } diff --git a/crates/valence_core/src/packet/s2c/play/player_list.rs b/crates/valence_core/src/packet/s2c/play/player_list.rs index 7f9e287da..fe0cb92ae 100644 --- a/crates/valence_core/src/packet/s2c/play/player_list.rs +++ b/crates/valence_core/src/packet/s2c/play/player_list.rs @@ -4,8 +4,8 @@ use std::io::Write; use bitfield_struct::bitfield; use uuid::Uuid; -use crate::packet::c2s::play::player_session::PlayerSessionData; use crate::game_mode::GameMode; +use crate::packet::c2s::play::player_session::PlayerSessionData; use crate::packet::var_int::VarInt; use crate::packet::{Decode, Encode}; use crate::property::Property; diff --git a/crates/valence_player_list/Cargo.toml b/crates/valence_player_list/Cargo.toml index d32714a5b..b1c2cab92 100644 --- a/crates/valence_player_list/Cargo.toml +++ b/crates/valence_player_list/Cargo.toml @@ -6,6 +6,7 @@ edition.workspace = true [dependencies] bevy_app.workspace = true bevy_ecs.workspace = true +rsa.workspace = true valence_core.workspace = true valence_client.workspace = true valence_instance.workspace = true diff --git a/crates/valence_player_list/src/lib.rs b/crates/valence_player_list/src/lib.rs index 14ce1ecbf..16f7d8edc 100644 --- a/crates/valence_player_list/src/lib.rs +++ b/crates/valence_player_list/src/lib.rs @@ -26,6 +26,7 @@ use uuid::Uuid; use valence_client::{Client, Ping, Properties, Username}; use valence_core::despawn::Despawned; use valence_core::game_mode::GameMode; +use valence_core::packet::c2s::play::player_session::PlayerSessionData; use valence_core::packet::encode::{PacketWriter, WritePacket}; use valence_core::packet::s2c::play::player_list::{Actions, Entry, PlayerListS2c}; use valence_core::packet::s2c::play::{PlayerListHeaderS2c, PlayerRemoveS2c}; From 3c943ba26bfc801864cc34ec3ae416bc4daa6498 Mon Sep 17 00:00:00 2001 From: Guac Date: Tue, 2 May 2023 23:26:28 -0700 Subject: [PATCH 17/32] Correct severity of debug output --- crates/valence_chat/src/lib.rs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/crates/valence_chat/src/lib.rs b/crates/valence_chat/src/lib.rs index 0acb64ea2..ca3e250ef 100644 --- a/crates/valence_chat/src/lib.rs +++ b/crates/valence_chat/src/lib.rs @@ -30,7 +30,7 @@ use rsa::{PaddingScheme, PublicKey, RsaPublicKey}; use rustc_hash::{FxHashMap, FxHashSet}; use sha1::{Digest, Sha1}; use sha2::Sha256; -use tracing::{debug, info, warn}; +use tracing::{info, trace, warn}; use uuid::Uuid; use valence_client::misc::{ChatMessage, MessageAcknowledgment, PlayerSession}; use valence_client::settings::ClientSettings; @@ -181,7 +181,7 @@ impl AcknowledgementValidator { list.push_back(m.signature); } else { // Client has acknowledged a non-existing message - warn!("Client has acknowledged a non-existing message"); + trace!("Client has acknowledged a non-existing message"); return None; } } else { @@ -189,7 +189,7 @@ impl AcknowledgementValidator { if matches!(acknowledged_message, Some(m) if !m.pending) { // The validator has an i-th message that has been validated but the client // claims that it hasn't been validated yet - warn!( + trace!( "The validator has an i-th message that has been validated but the client \ claims that it hasn't been validated yet" ); @@ -453,8 +453,6 @@ fn handle_message_acknowledgement( }); continue; } - - debug!("Acknowledgement from '{:?}'", username.0); } } From 7e461764246f978dfaecd2bda851ae8f98f77627 Mon Sep 17 00:00:00 2001 From: Guac Date: Thu, 4 May 2023 16:48:37 -0700 Subject: [PATCH 18/32] Minor changes More to come --- crates/valence/examples/chat.rs | 16 +-- crates/valence/src/lib.rs | 8 +- crates/valence_chat/README.md | 6 +- crates/valence_chat/src/lib.rs | 99 ++++++++---------- .../yggdrasil_session_pubkey.der | Bin 5 files changed, 57 insertions(+), 72 deletions(-) rename {assets => crates/valence_chat}/yggdrasil_session_pubkey.der (100%) diff --git a/crates/valence/examples/chat.rs b/crates/valence/examples/chat.rs index 6f7442572..d13d4bda6 100644 --- a/crates/valence/examples/chat.rs +++ b/crates/valence/examples/chat.rs @@ -3,7 +3,6 @@ use tracing::warn; use valence::client::despawn_disconnected_clients; use valence::client::misc::CommandExecution; -use valence::entity::player::PlayerEntityBundle; use valence::prelude::*; const SPAWN_Y: i32 = 64; @@ -46,20 +45,15 @@ fn setup( } fn init_clients( - mut clients: Query<(Entity, &UniqueId, &mut Client, &mut GameMode), Added>, + mut clients: Query<(&mut Client, &mut Location, &mut Position, &mut GameMode), Added>, instances: Query>, - mut commands: Commands, ) { - for (entity, uuid, mut client, mut game_mode) in &mut clients { + for (mut client, mut loc, mut pos, mut game_mode) in &mut clients { *game_mode = GameMode::Creative; - client.send_message("Welcome to Valence! Say something.".italic()); + loc.0 = instances.single(); + pos.set([0.0, SPAWN_Y as f64 + 1.0, 0.0]); - commands.entity(entity).insert(PlayerEntityBundle { - location: Location(instances.single()), - position: Position::new([0.0, SPAWN_Y as f64 + 1.0, 0.0]), - uuid: *uuid, - ..Default::default() - }); + client.send_message("Welcome to Valence! Say something.".italic()); } } diff --git a/crates/valence/src/lib.rs b/crates/valence/src/lib.rs index 6b7045e32..b2f116140 100644 --- a/crates/valence/src/lib.rs +++ b/crates/valence/src/lib.rs @@ -29,7 +29,7 @@ mod tests; #[cfg(feature = "anvil")] pub use valence_anvil as anvil; #[cfg(feature = "chat")] -pub use valence_chat as secure_chat; +pub use valence_chat as chat; pub use valence_core::*; #[cfg(feature = "inventory")] pub use valence_inventory as inventory; @@ -61,6 +61,8 @@ pub mod prelude { pub use biome::{Biome, BiomeId, BiomeRegistry}; pub use block::{BlockKind, BlockState, PropName, PropValue}; pub use block_pos::BlockPos; + #[cfg(feature = "chat")] + pub use chat::chat_type::{ChatType, ChatTypeRegistry}; pub use chunk_pos::{ChunkPos, ChunkView}; pub use client::action::*; pub use client::command::*; @@ -98,8 +100,6 @@ pub mod prelude { pub use packet::s2c::play::particle::Particle; #[cfg(feature = "player_list")] pub use player_list::{PlayerList, PlayerListEntry}; - #[cfg(feature = "chat")] - pub use secure_chat::chat_type::{ChatType, ChatTypeRegistry}; pub use text::{Color, Text, TextFormat}; #[cfg(feature = "advancement")] pub use valence_advancement::{ @@ -163,7 +163,7 @@ impl PluginGroup for DefaultPlugins { #[cfg(feature = "chat")] { - group = group.add(valence_chat::SecureChatPlugin); + group = group.add(valence_chat::ChatPlugin); } group diff --git a/crates/valence_chat/README.md b/crates/valence_chat/README.md index b26f0bb3e..c814f8553 100644 --- a/crates/valence_chat/README.md +++ b/crates/valence_chat/README.md @@ -4,7 +4,5 @@ Provides support for cryptographically verified chat messaging on the server. This crate contains the secure chat plugin as well as chat types and the chat type registry. Minecraft's default chat types are added to the registry by default. Chat types contain information about how chat is styled, such as the chat color. -### **NOTE:** -- Modifying the chat type registry after the server has started can -break invariants within instances and clients! Make sure there are no -instances or clients spawned before mutating. \ No newline at end of file + +This crate also contains the `yggdrasil_session_pubkey.der` file which is an encoded format of Mojang's public key. This is necessary to verify the integrity of our clients' public session key, which is used for validating chat messages. In reality Mojang's key should never change in order to maintain backwards compatibility with older versions, but if it does it can be extracted from any minecraft server jar. \ No newline at end of file diff --git a/crates/valence_chat/src/lib.rs b/crates/valence_chat/src/lib.rs index ca3e250ef..0c6d05f92 100644 --- a/crates/valence_chat/src/lib.rs +++ b/crates/valence_chat/src/lib.rs @@ -22,6 +22,7 @@ pub mod chat_type; use std::collections::VecDeque; use std::time::SystemTime; +use anyhow::bail; use bevy_app::prelude::*; use bevy_ecs::prelude::*; use chat_type::ChatTypePlugin; @@ -30,7 +31,7 @@ use rsa::{PaddingScheme, PublicKey, RsaPublicKey}; use rustc_hash::{FxHashMap, FxHashSet}; use sha1::{Digest, Sha1}; use sha2::Sha256; -use tracing::{info, trace, warn}; +use tracing::{info, warn}; use uuid::Uuid; use valence_client::misc::{ChatMessage, MessageAcknowledgment, PlayerSession}; use valence_client::settings::ClientSettings; @@ -51,7 +52,33 @@ use valence_core::translation_key::{ use valence_core::uuid::UniqueId; use valence_player_list::{ChatSession, PlayerListEntry}; -const MOJANG_KEY_DATA: &[u8] = include_bytes!("../../../assets/yggdrasil_session_pubkey.der"); +const MOJANG_KEY_DATA: &[u8] = include_bytes!("../yggdrasil_session_pubkey.der"); + +pub struct ChatPlugin; + +impl Plugin for ChatPlugin { + fn build(&self, app: &mut bevy_app::App) { + let mojang_pub_key = RsaPublicKey::from_public_key_der(MOJANG_KEY_DATA) + .expect("Error creating Mojang public key"); + + app.add_plugin(ChatTypePlugin) + .insert_resource(MojangServicesState::new(mojang_pub_key)) + .add_systems( + ( + init_chat_states, + handle_session_events + .after(init_chat_states) + .before(handle_message_events), + handle_message_acknowledgement + .after(init_chat_states) + .before(handle_message_events), + handle_message_events.after(init_chat_states), + ) + .in_base_set(CoreSet::PostUpdate) + .before(FlushPacketsSet), + ); + } +} #[derive(Resource)] struct MojangServicesState { @@ -149,10 +176,9 @@ impl AcknowledgementValidator { &mut self, acknowledgements: &[u8; 3], message_index: i32, - ) -> Option> { + ) -> anyhow::Result> { if !self.remove_until(message_index) { - // Invalid message index - return None; + bail!("Invalid message index"); } let acknowledged_count = { @@ -164,15 +190,13 @@ impl AcknowledgementValidator { }; if acknowledged_count > 20 { - // Too many message acknowledgements, protocol error? - return None; + bail!("Too many message acknowledgements, protocol error?"); } let mut list = VecDeque::with_capacity(acknowledged_count); for i in 0..20 { let acknowledgement = acknowledgements[i >> 3] & (0b1 << (i % 8)) != 0; - // SAFETY: The length of messages is never less than 20 - let acknowledged_message = unsafe { self.messages.get_unchecked_mut(i) }; + let acknowledged_message = &mut self.messages[i]; // Client has acknowledged the i-th message if acknowledgement { // The validator has the i-th message @@ -181,27 +205,23 @@ impl AcknowledgementValidator { list.push_back(m.signature); } else { // Client has acknowledged a non-existing message - trace!("Client has acknowledged a non-existing message"); - return None; + bail!("Client has acknowledged a non-existing message"); } } else { // Client has not acknowledged the i-th message if matches!(acknowledged_message, Some(m) if !m.pending) { // The validator has an i-th message that has been validated but the client // claims that it hasn't been validated yet - trace!( + bail!( "The validator has an i-th message that has been validated but the client \ claims that it hasn't been validated yet" ); - return None; } // Honestly not entirely sure why this is done - if acknowledged_message.is_some() { - *acknowledged_message = None; - } + *acknowledged_message = None; } } - Some(list) + Ok(list) } /// The number of pending messages in the validator. @@ -310,32 +330,6 @@ impl MessageSignatureStorage { } } -pub struct SecureChatPlugin; - -impl Plugin for SecureChatPlugin { - fn build(&self, app: &mut bevy_app::App) { - let mojang_pub_key = RsaPublicKey::from_public_key_der(MOJANG_KEY_DATA) - .expect("Error creating Mojang public key"); - - app.add_plugin(ChatTypePlugin) - .insert_resource(MojangServicesState::new(mojang_pub_key)) - .add_systems( - ( - init_chat_states, - handle_session_events - .after(init_chat_states) - .before(handle_message_events), - handle_message_acknowledgement - .after(init_chat_states) - .before(handle_message_events), - handle_message_events.after(init_chat_states), - ) - .in_base_set(CoreSet::PostUpdate) - .before(FlushPacketsSet), - ); - } -} - fn init_chat_states(clients: Query>, mut commands: Commands) { for entity in clients.iter() { commands.entity(entity).insert(ChatState::default()); @@ -369,15 +363,11 @@ fn handle_session_events( continue; } - // Serialize the session data. - let mut serialized = Vec::with_capacity(318); - serialized.extend_from_slice(uuid.0.into_bytes().as_slice()); - serialized.extend_from_slice(session.session_data.expires_at.to_be_bytes().as_ref()); - serialized.extend_from_slice(session.session_data.public_key_data.as_ref()); - // Hash the session data using the SHA-1 algorithm. let mut hasher = Sha1::new(); - hasher.update(&serialized); + hasher.update(uuid.0.into_bytes()); + hasher.update(session.session_data.expires_at.to_be_bytes()); + hasher.update(&session.session_data.public_key_data); let hash = hasher.finalize(); // Verify the session data using Mojang's public key and the hashed session data @@ -507,15 +497,18 @@ fn handle_message_events( .validator .validate(&message.acknowledgements, message.message_index) { - None => { - warn!("Failed to validate acknowledgements from `{}`", username.0); + Err(error) => { + warn!( + "Failed to validate acknowledgements from `{}`: {}", + username.0, error + ); commands.add(DisconnectClient { client: message.client, reason: Text::translate(MULTIPLAYER_DISCONNECT_CHAT_VALIDATION_FAILED, []), }); continue; } - Some(last_seen) => last_seen, + Ok(last_seen) => last_seen, }; let Some(link) = &state.chain.next_link() else { diff --git a/assets/yggdrasil_session_pubkey.der b/crates/valence_chat/yggdrasil_session_pubkey.der similarity index 100% rename from assets/yggdrasil_session_pubkey.der rename to crates/valence_chat/yggdrasil_session_pubkey.der From 59391efd9bce171f3c3ac4a03e72680273e50401 Mon Sep 17 00:00:00 2001 From: Guac Date: Mon, 25 Sep 2023 10:38:50 -0600 Subject: [PATCH 19/32] Transfer Some minor changes to retain before updating --- crates/README.md | 1 + crates/valence/Cargo.toml | 5 +- crates/valence/examples/block_entities.rs | 3 +- crates/valence/examples/chat.rs | 22 +- crates/valence_chat/Cargo.toml | 13 +- crates/valence_chat/src/chat_type.rs | 34 +- crates/valence_chat/src/lib.rs | 444 +++++++++++++++------- crates/valence_client/Cargo.toml | 4 + crates/valence_client/src/chat.rs | 32 ++ crates/valence_client/src/lib.rs | 2 + crates/valence_client/src/misc.rs | 69 +--- 11 files changed, 390 insertions(+), 239 deletions(-) create mode 100644 crates/valence_client/src/chat.rs diff --git a/crates/README.md b/crates/README.md index f17011ef7..9aa54c553 100644 --- a/crates/README.md +++ b/crates/README.md @@ -22,4 +22,5 @@ graph TD anvil --> instance entity --> block advancement --> client + chat --> client ``` diff --git a/crates/valence/Cargo.toml b/crates/valence/Cargo.toml index 995cb3f07..91507b616 100644 --- a/crates/valence/Cargo.toml +++ b/crates/valence/Cargo.toml @@ -11,13 +11,14 @@ keywords = ["minecraft", "gamedev", "server", "ecs"] categories = ["game-engines"] [features] -default = ["network", "player_list", "inventory", "anvil", "advancement", "chat"] +default = ["network", "player_list", "inventory", "anvil", "advancement", "secure_chat"] network = ["dep:valence_network"] player_list = ["dep:valence_player_list"] inventory = ["dep:valence_inventory"] anvil = ["dep:valence_anvil"] advancement = ["dep:valence_advancement"] -chat = ["dep:valence_chat"] +chat = ["dep:valence_chat", "valence_client/chat"] +secure_chat = ["chat", "valence_chat?/secure", "valence_client/secure_chat"] [dependencies] bevy_app.workspace = true diff --git a/crates/valence/examples/block_entities.rs b/crates/valence/examples/block_entities.rs index b94a89d75..5974c1f0a 100644 --- a/crates/valence/examples/block_entities.rs +++ b/crates/valence/examples/block_entities.rs @@ -1,6 +1,7 @@ #![allow(clippy::type_complexity)] -use valence::client::misc::{ChatMessage, InteractBlock}; +use valence::client::chat::ChatMessage; +use valence::client::misc::InteractBlock; use valence::nbt::{compound, List}; use valence::prelude::*; diff --git a/crates/valence/examples/chat.rs b/crates/valence/examples/chat.rs index d13d4bda6..a11c94f82 100644 --- a/crates/valence/examples/chat.rs +++ b/crates/valence/examples/chat.rs @@ -1,8 +1,9 @@ #![allow(clippy::type_complexity)] use tracing::warn; +use valence::chat::ChatState; +use valence::client::chat::{ChatMessage, CommandExecution}; use valence::client::despawn_disconnected_clients; -use valence::client::misc::CommandExecution; use valence::prelude::*; const SPAWN_Y: i32 = 64; @@ -16,6 +17,7 @@ pub fn main() { .add_systems(( init_clients, despawn_disconnected_clients, + handle_message_events.in_schedule(EventLoopSchedule), handle_command_events.in_schedule(EventLoopSchedule), )) .run(); @@ -57,6 +59,24 @@ fn init_clients( } } +fn handle_message_events( + mut clients: Query<(&mut Client, &mut ChatState)>, + names: Query<&Username>, + mut messages: EventReader, +) { + for message in messages.iter() { + let sender_name = names.get(message.client).expect("Error getting username"); + // Need to find better way. Username is sender, while client and chat state are + // recievers. Maybe try to add a chat feature to Client. + for (mut client, mut state) in clients.iter_mut() { + state + .as_mut() + .send_chat_message(client.as_mut(), sender_name, message) + .expect("Error sending message"); + } + } +} + fn handle_command_events( mut clients: Query<&mut Client>, mut commands: EventReader, diff --git a/crates/valence_chat/Cargo.toml b/crates/valence_chat/Cargo.toml index 2ea10e7f7..ee66a3b51 100644 --- a/crates/valence_chat/Cargo.toml +++ b/crates/valence_chat/Cargo.toml @@ -3,17 +3,20 @@ name = "valence_chat" version.workspace = true edition.workspace = true +[features] +secure = ["dep:rsa", "dep:rustc-hash", "dep:sha1", "dep:sha2", "valence_client/secure_chat"] + [dependencies] anyhow.workspace = true bevy_app.workspace = true bevy_ecs.workspace = true -rsa.workspace = true -rustc-hash.workspace = true -sha1 = { workspace = true, features = ["oid"] } -sha2 = { workspace = true, features = ["oid"] } +rsa = { workspace = true, optional = true } +rustc-hash = { workspace = true, optional = true } +sha1 = { workspace = true, features = ["oid"], optional = true } +sha2 = { workspace = true, features = ["oid"], optional = true } tracing.workspace = true valence_core.workspace = true -valence_client.workspace = true +valence_client = { workspace = true, features = ["chat"], optional = true } valence_instance.workspace = true valence_nbt.workspace = true valence_player_list.workspace = true diff --git a/crates/valence_chat/src/chat_type.rs b/crates/valence_chat/src/chat_type.rs index 2222cd7e5..c03cb094e 100644 --- a/crates/valence_chat/src/chat_type.rs +++ b/crates/valence_chat/src/chat_type.rs @@ -18,6 +18,23 @@ use valence_core::text::Color; use valence_nbt::{compound, Compound, List, Value}; use valence_registry::{RegistryCodec, RegistryCodecSet, RegistryValue}; +pub(crate) struct ChatTypePlugin; + +impl Plugin for ChatTypePlugin { + fn build(&self, app: &mut bevy_app::App) { + app.insert_resource(ChatTypeRegistry { + id_to_chat_type: vec![], + }) + .add_systems( + (update_chat_type_registry, remove_chat_types_from_registry) + .chain() + .in_base_set(CoreSet::PostUpdate) + .before(RegistryCodecSet), + ) + .add_startup_system(load_default_chat_types.in_base_set(StartupSet::PreStartup)); + } +} + #[derive(Resource)] pub struct ChatTypeRegistry { id_to_chat_type: Vec, @@ -119,23 +136,6 @@ pub struct ChatTypeParameters { content: bool, } -pub(crate) struct ChatTypePlugin; - -impl Plugin for ChatTypePlugin { - fn build(&self, app: &mut bevy_app::App) { - app.insert_resource(ChatTypeRegistry { - id_to_chat_type: vec![], - }) - .add_systems( - (update_chat_type_registry, remove_chat_types_from_registry) - .chain() - .in_base_set(CoreSet::PostUpdate) - .before(RegistryCodecSet), - ) - .add_startup_system(load_default_chat_types.in_base_set(StartupSet::PreStartup)); - } -} - fn load_default_chat_types( mut reg: ResMut, codec: Res, diff --git a/crates/valence_chat/src/lib.rs b/crates/valence_chat/src/lib.rs index 0c6d05f92..00ba27a8e 100644 --- a/crates/valence_chat/src/lib.rs +++ b/crates/valence_chat/src/lib.rs @@ -19,72 +19,101 @@ pub mod chat_type; +#[cfg(feature = "secure")] use std::collections::VecDeque; use std::time::SystemTime; -use anyhow::bail; use bevy_app::prelude::*; use bevy_ecs::prelude::*; use chat_type::ChatTypePlugin; -use rsa::pkcs8::DecodePublicKey; -use rsa::{PaddingScheme, PublicKey, RsaPublicKey}; -use rustc_hash::{FxHashMap, FxHashSet}; -use sha1::{Digest, Sha1}; -use sha2::Sha256; -use tracing::{info, warn}; -use uuid::Uuid; -use valence_client::misc::{ChatMessage, MessageAcknowledgment, PlayerSession}; +use tracing::warn; +use valence_client::chat::{ChatMessage, CommandExecution}; +use valence_client::event_loop::{EventLoopSchedule, EventLoopSet, PacketEvent, RunEventLoopSet}; use valence_client::settings::ClientSettings; -use valence_client::{Client, DisconnectClient, FlushPacketsSet, Username}; +use valence_client::{Client, SpawnClientsSet}; use valence_core::packet::c2s::play::client_settings::ChatMode; +use valence_core::packet::c2s::play::{ChatMessageC2s, CommandExecutionC2s}; use valence_core::packet::encode::WritePacket; use valence_core::packet::message_signature::MessageSignature; use valence_core::packet::s2c::play::chat_message::MessageFilterType; -use valence_core::packet::s2c::play::ChatMessageS2c; +use valence_core::packet::s2c::play::{ChatMessageS2c, ProfilelessChatMessageS2c}; use valence_core::text::{Color, Text, TextFormat}; -use valence_core::translation_key::{ - CHAT_DISABLED_CHAIN_BROKEN, CHAT_DISABLED_EXPIRED_PROFILE_KEY, - CHAT_DISABLED_MISSING_PROFILE_KEY, CHAT_DISABLED_OPTIONS, - MULTIPLAYER_DISCONNECT_CHAT_VALIDATION_FAILED, MULTIPLAYER_DISCONNECT_EXPIRED_PUBLIC_KEY, - MULTIPLAYER_DISCONNECT_INVALID_PUBLIC_KEY_SIGNATURE, MULTIPLAYER_DISCONNECT_OUT_OF_ORDER_CHAT, - MULTIPLAYER_DISCONNECT_TOO_MANY_PENDING_CHATS, MULTIPLAYER_DISCONNECT_UNSIGNED_CHAT, +use valence_core::translation_key::{CHAT_DISABLED_OPTIONS, DISCONNECT_GENERIC_REASON}; +#[cfg(feature = "secure")] +use { + anyhow::bail, + rsa::pkcs8::DecodePublicKey, + rsa::{PaddingScheme, PublicKey, RsaPublicKey}, + rustc_hash::{FxHashMap, FxHashSet}, + sha1::{Digest, Sha1}, + sha2::Sha256, + uuid::Uuid, + valence_client::chat::ChatMessageType, + valence_client::{DisconnectClient, Username}, + valence_core::packet::c2s::play::{MessageAcknowledgmentC2s, PlayerSessionC2s}, + valence_core::translation_key::{ + CHAT_DISABLED_CHAIN_BROKEN, CHAT_DISABLED_EXPIRED_PROFILE_KEY, + CHAT_DISABLED_MISSING_PROFILE_KEY, MULTIPLAYER_DISCONNECT_CHAT_VALIDATION_FAILED, + MULTIPLAYER_DISCONNECT_EXPIRED_PUBLIC_KEY, + MULTIPLAYER_DISCONNECT_INVALID_PUBLIC_KEY_SIGNATURE, + MULTIPLAYER_DISCONNECT_OUT_OF_ORDER_CHAT, MULTIPLAYER_DISCONNECT_TOO_MANY_PENDING_CHATS, + MULTIPLAYER_DISCONNECT_UNSIGNED_CHAT, + }, + valence_core::uuid::UniqueId, + valence_player_list::{ChatSession, PlayerListEntry}, }; -use valence_core::uuid::UniqueId; -use valence_player_list::{ChatSession, PlayerListEntry}; +#[cfg(feature = "secure")] const MOJANG_KEY_DATA: &[u8] = include_bytes!("../yggdrasil_session_pubkey.der"); pub struct ChatPlugin; impl Plugin for ChatPlugin { fn build(&self, app: &mut bevy_app::App) { - let mojang_pub_key = RsaPublicKey::from_public_key_der(MOJANG_KEY_DATA) - .expect("Error creating Mojang public key"); - app.add_plugin(ChatTypePlugin) - .insert_resource(MojangServicesState::new(mojang_pub_key)) + .add_event::() + .add_event::() + .add_system( + init_chat_states + .in_base_set(CoreSet::PreUpdate) + .after(SpawnClientsSet) + .before(RunEventLoopSet), + ) .add_systems( ( - init_chat_states, - handle_session_events - .after(init_chat_states) - .before(handle_message_events), - handle_message_acknowledgement - .after(init_chat_states) - .before(handle_message_events), - handle_message_events.after(init_chat_states), + #[cfg(feature = "secure")] + handle_acknowledgement_packets, + #[cfg(not(feature = "secure"))] + handle_message_packets, + handle_command_packets, ) - .in_base_set(CoreSet::PostUpdate) - .before(FlushPacketsSet), + .in_base_set(EventLoopSet::PreUpdate) + .in_schedule(EventLoopSchedule), ); + + #[cfg(feature = "secure")] + { + let mojang_pub_key = RsaPublicKey::from_public_key_der(MOJANG_KEY_DATA) + .expect("Error creating Mojang public key"); + + app.insert_resource(MojangServicesState::new(mojang_pub_key)) + .add_systems( + (handle_session_packets, handle_message_packets) + .chain() + .in_base_set(EventLoopSet::PreUpdate) + .in_schedule(EventLoopSchedule), + ); + } } } +#[cfg(feature = "secure")] #[derive(Resource)] struct MojangServicesState { public_key: RsaPublicKey, } +#[cfg(feature = "secure")] impl MojangServicesState { fn new(public_key: RsaPublicKey) -> Self { Self { public_key } @@ -92,10 +121,13 @@ impl MojangServicesState { } #[derive(Debug, Component)] -struct ChatState { - last_message_timestamp: u64, +pub struct ChatState { + pub last_message_timestamp: u64, + #[cfg(feature = "secure")] validator: AcknowledgementValidator, + #[cfg(feature = "secure")] chain: MessageChain, + #[cfg(feature = "secure")] signature_storage: MessageSignatureStorage, } @@ -106,29 +138,88 @@ impl Default for ChatState { .duration_since(SystemTime::UNIX_EPOCH) .expect("Unable to get Unix time") .as_millis() as u64, + #[cfg(feature = "secure")] validator: AcknowledgementValidator::new(), + #[cfg(feature = "secure")] chain: MessageChain::new(), + #[cfg(feature = "secure")] signature_storage: MessageSignatureStorage::new(), } } } impl ChatState { + pub fn send_chat_message( + &mut self, + client: &mut Client, + username: &Username, + message: &ChatMessage, + ) -> anyhow::Result<()> { + match &message.message_type { + ChatMessageType::Signed { + salt, + signature, + message_index, + last_seen, + sender, + } => { + // Create a list of messages that have been seen by the client. + let previous = last_seen + .iter() + .map(|sig| match self.signature_storage.index_of(sig) { + Some(index) => MessageSignature::ByIndex(index), + None => MessageSignature::BySignature(sig), + }) + .collect::>(); + + client.write_packet(&ChatMessageS2c { + sender: *sender, + index: (*message_index).into(), + message_signature: Some((*signature).as_ref()), + message: message.message.as_ref(), + time_stamp: message.timestamp, + salt: *salt, + previous_messages: previous, + unsigned_content: None, + filter_type: MessageFilterType::PassThrough, + chat_type: 0.into(), // TODO: Make chat type for player messages selectable + network_name: Text::from(username.0.clone()).into(), + network_target_name: None, + }); + // Add pending acknowledgement. + self.add_pending(last_seen, signature); + if self.validator.message_count() > 4096 { + warn!("User has too many pending chats `{}`", username.0); + bail!(MULTIPLAYER_DISCONNECT_TOO_MANY_PENDING_CHATS); + } + } + ChatMessageType::Unsigned => client.write_packet(&ProfilelessChatMessageS2c { + message: Text::from(message.message.to_string()).into(), + chat_type: 0.into(), + chat_type_name: Text::from(username.0.clone()).into(), + target_name: None, + }), + } + Ok(()) + } + /// Updates the chat state's previously seen signatures with a new one /// `signature`. - /// Warning this modifies `last_seen`. - fn add_pending(&mut self, last_seen: &mut VecDeque<[u8; 256]>, signature: [u8; 256]) { - self.signature_storage.add(last_seen, &signature); - self.validator.add_pending(&signature); + #[cfg(feature = "secure")] + fn add_pending(&mut self, last_seen: &Vec<[u8; 256]>, signature: &[u8; 256]) { + self.signature_storage.add(last_seen, signature); + self.validator.add_pending(signature); } } +#[cfg(feature = "secure")] #[derive(Clone, Debug)] struct AcknowledgementValidator { messages: Vec>, last_signature: Option<[u8; 256]>, } +#[cfg(feature = "secure")] impl AcknowledgementValidator { fn new() -> Self { Self { @@ -176,7 +267,7 @@ impl AcknowledgementValidator { &mut self, acknowledgements: &[u8; 3], message_index: i32, - ) -> anyhow::Result> { + ) -> anyhow::Result> { if !self.remove_until(message_index) { bail!("Invalid message index"); } @@ -193,7 +284,7 @@ impl AcknowledgementValidator { bail!("Too many message acknowledgements, protocol error?"); } - let mut list = VecDeque::with_capacity(acknowledged_count); + let mut list = Vec::with_capacity(acknowledged_count); for i in 0..20 { let acknowledgement = acknowledgements[i >> 3] & (0b1 << (i % 8)) != 0; let acknowledged_message = &mut self.messages[i]; @@ -202,7 +293,7 @@ impl AcknowledgementValidator { // The validator has the i-th message if let Some(m) = acknowledged_message { m.pending = false; - list.push_back(m.signature); + list.push(m.signature); } else { // Client has acknowledged a non-existing message bail!("Client has acknowledged a non-existing message"); @@ -230,17 +321,20 @@ impl AcknowledgementValidator { } } +#[cfg(feature = "secure")] #[derive(Clone, Debug)] struct AcknowledgedMessage { signature: [u8; 256], pending: bool, } +#[cfg(feature = "secure")] #[derive(Clone, Default, Debug)] struct MessageChain { link: Option, } +#[cfg(feature = "secure")] impl MessageChain { fn new() -> Self { Self::default() @@ -258,6 +352,7 @@ impl MessageChain { } } +#[cfg(feature = "secure")] #[derive(Copy, Clone, Debug)] struct MessageLink { index: i32, @@ -265,6 +360,7 @@ struct MessageLink { session_id: Uuid, } +#[cfg(feature = "secure")] impl MessageLink { fn update_hash(&self, hasher: &mut impl Digest) { hasher.update(self.sender.into_bytes()); @@ -273,12 +369,14 @@ impl MessageLink { } } +#[cfg(feature = "secure")] #[derive(Clone, Debug)] struct MessageSignatureStorage { signatures: [Option<[u8; 256]>; 128], indices: FxHashMap<[u8; 256], i32>, } +#[cfg(feature = "secure")] impl Default for MessageSignatureStorage { fn default() -> Self { Self { @@ -288,6 +386,7 @@ impl Default for MessageSignatureStorage { } } +#[cfg(feature = "secure")] impl MessageSignatureStorage { fn new() -> Self { Self::default() @@ -302,30 +401,39 @@ impl MessageSignatureStorage { /// `signature` to the storage. /// /// Warning: this consumes `last_seen`. - fn add(&mut self, last_seen: &mut VecDeque<[u8; 256]>, signature: &[u8; 256]) { - last_seen.push_back(*signature); + fn add(&mut self, last_seen: &Vec<[u8; 256]>, signature: &[u8; 256]) { let mut sig_set = FxHashSet::default(); - for sig in last_seen.iter() { - sig_set.insert(*sig); - } - for i in 0..128 { - if last_seen.is_empty() { + + last_seen + .iter() + .chain(std::iter::once(signature)) + .for_each(|sig| { + sig_set.insert(*sig); + }); + + let mut retained_sigs = VecDeque::new(); + let mut index = 0usize; + let mut seen_iter = last_seen.iter().chain(std::iter::once(signature)).rev(); + + while let Some(seen_sig) = seen_iter.next().or(retained_sigs.pop_front().as_ref()) { + if index > 127 { return; } - // Remove old message - let message_sig_data = self.signatures[i]; - // Add previously seen message - self.signatures[i] = last_seen.pop_back(); - if let Some(data) = self.signatures[i] { - self.indices.insert(data, i as i32); - } - // Reinsert old message if it is not already in last_seen - if let Some(data) = message_sig_data { + // Remove the old signature + let previous_sig = self.signatures[index]; + // Add the new signature + self.signatures[index] = Some(*seen_sig); + self.indices.insert(*seen_sig, index as i32); + // Reinsert old signature if it is not already in `last_seen` + if let Some(data) = previous_sig { + // Remove the index for the old sig self.indices.remove(&data); + // If the old sig is still unique, reinsert if sig_set.insert(data) { - last_seen.push_front(data); + retained_sigs.push_back(data); } } + index += 1; } } } @@ -336,14 +444,19 @@ fn init_chat_states(clients: Query>, mut commands: Command } } -fn handle_session_events( +#[cfg(feature = "secure")] +fn handle_session_packets( services_state: Res, mut clients: Query<(&UniqueId, &Username, &mut ChatState), With>, - mut sessions: EventReader, + mut packets: EventReader, mut commands: Commands, ) { - for session in sessions.iter() { - let Ok((uuid, username, mut state)) = clients.get_mut(session.client) else { + for packet in packets.iter() { + let Some(session) = packet.decode::() else { + continue; + }; + + let Ok((uuid, username, mut state)) = clients.get_mut(packet.client) else { warn!("Unable to find client in player list for session"); continue; }; @@ -353,11 +466,11 @@ fn handle_session_events( .duration_since(SystemTime::UNIX_EPOCH) .expect("Unable to get Unix time") .as_millis() - >= session.session_data.expires_at as u128 + >= session.0.expires_at as u128 { warn!("Failed to validate profile key: expired public key"); commands.add(DisconnectClient { - client: session.client, + client: packet.client, reason: Text::translate(MULTIPLAYER_DISCONNECT_EXPIRED_PUBLIC_KEY, []), }); continue; @@ -366,8 +479,8 @@ fn handle_session_events( // Hash the session data using the SHA-1 algorithm. let mut hasher = Sha1::new(); hasher.update(uuid.0.into_bytes()); - hasher.update(session.session_data.expires_at.to_be_bytes()); - hasher.update(&session.session_data.public_key_data); + hasher.update(session.0.expires_at.to_be_bytes()); + hasher.update(&session.0.public_key_data); let hash = hasher.finalize(); // Verify the session data using Mojang's public key and the hashed session data @@ -377,33 +490,33 @@ fn handle_session_events( .verify( PaddingScheme::new_pkcs1v15_sign::(), &hash, - session.session_data.key_signature.as_ref(), + session.0.key_signature.as_ref(), ) .is_err() { warn!("Failed to validate profile key: invalid public key signature"); commands.add(DisconnectClient { - client: session.client, + client: packet.client, reason: Text::translate(MULTIPLAYER_DISCONNECT_INVALID_PUBLIC_KEY_SIGNATURE, []), }); } // Decode the player's session public key from the data. if let Ok(public_key) = - RsaPublicKey::from_public_key_der(session.session_data.public_key_data.as_ref()) + RsaPublicKey::from_public_key_der(session.0.public_key_data.as_ref()) { // Update the player's chat state data with the new player session data. state.chain.link = Some(MessageLink { index: 0, sender: uuid.0, - session_id: session.session_data.session_id, + session_id: session.0.session_id, }); // Add the chat session data to player. // The player list will then send this new session data to the other clients. - commands.entity(session.client).insert(ChatSession { + commands.entity(packet.client).insert(ChatSession { public_key, - session_data: session.session_data.clone(), + session_data: session.0.into_owned(), }); } else { // This shouldn't happen considering that it is highly unlikely that Mojang @@ -411,9 +524,9 @@ fn handle_session_events( // key signature has been verified. warn!("Received malformed profile key data from '{}'", username.0); commands.add(DisconnectClient { - client: session.client, + client: packet.client, reason: Text::translate( - MULTIPLAYER_DISCONNECT_EXPIRED_PUBLIC_KEY, + DISCONNECT_GENERIC_REASON, ["Malformed profile key data".color(Color::RED)], ), }); @@ -421,24 +534,32 @@ fn handle_session_events( } } -fn handle_message_acknowledgement( +#[cfg(feature = "secure")] +fn handle_acknowledgement_packets( mut clients: Query<(&Username, &mut ChatState)>, - mut acknowledgements: EventReader, + mut packets: EventReader, mut commands: Commands, ) { - for acknowledgement in acknowledgements.iter() { - let Ok((username, mut state)) = clients.get_mut(acknowledgement.client) else { + for packet in packets.iter() { + let Some(acknowledgement) = packet.decode::() else { + continue; + }; + + let Ok((username, mut state)) = clients.get_mut(packet.client) else { warn!("Unable to find client for acknowledgement"); continue; }; - if !state.validator.remove_until(acknowledgement.message_index) { + if !state + .validator + .remove_until(acknowledgement.message_index.0) + { warn!( "Failed to validate message acknowledgement from '{:?}'", username.0 ); commands.add(DisconnectClient { - client: acknowledgement.client, + client: packet.client, reason: Text::translate(MULTIPLAYER_DISCONNECT_CHAT_VALIDATION_FAILED, []), }); continue; @@ -446,27 +567,24 @@ fn handle_message_acknowledgement( } } -fn handle_message_events( +#[cfg(feature = "secure")] +fn handle_message_packets( mut clients: Query< - (&mut Client, &mut ChatState, &Username, &ClientSettings), + (&mut ChatState, &mut Client, &Username, &ClientSettings), With, >, sessions: Query<&ChatSession, With>, - mut messages: EventReader, + mut packets: EventReader, + mut message_events: EventWriter, mut commands: Commands, ) { - for message in messages.iter() { - let Ok((mut client, mut state, username, settings)) = clients.get_mut(message.client) else { - warn!("Unable to find client for message '{:?}'", message); + for packet in packets.iter() { + let Some(message) = packet.decode::() else { continue; }; - let Ok(chat_session) = sessions.get_component::(message.client) else { - warn!("Player `{}` doesn't have a chat session", username.0); - commands.add(DisconnectClient { - client: message.client, - reason: Text::translate(CHAT_DISABLED_MISSING_PROFILE_KEY, []) - }); + let Ok((mut state, mut client, username, settings)) = clients.get_mut(packet.client) else { + warn!("Unable to find client for message '{:?}'", message); continue; }; @@ -480,11 +598,10 @@ fn handle_message_events( if message.timestamp < state.last_message_timestamp { warn!( "{:?} sent out-of-order chat: '{:?}'", - username.0, - message.message.as_ref() + username.0, message.message ); commands.add(DisconnectClient { - client: message.client, + client: packet.client, reason: Text::translate(MULTIPLAYER_DISCONNECT_OUT_OF_ORDER_CHAT, []), }); continue; @@ -492,10 +609,27 @@ fn handle_message_events( state.last_message_timestamp = message.timestamp; + // Check if the message is signed + let Some(message_signature) = message.signature else { + // TODO: Cleanup + warn!("Received unsigned chat message from `{}`", username.0); + /*commands.add(DisconnectClient { + client: packet.client, + reason: Text::translate(MULTIPLAYER_DISCONNECT_UNSIGNED_CHAT, []) + });*/ + message_events.send(ChatMessage { + client: packet.client, + message: message.message.into(), + timestamp: message.timestamp, + message_type: ChatMessageType::Unsigned, + }); + continue; + }; + // Validate the message acknowledgements. let last_seen = match state .validator - .validate(&message.acknowledgements, message.message_index) + .validate(&message.acknowledgement, message.message_index.0) { Err(error) => { warn!( @@ -503,7 +637,7 @@ fn handle_message_events( username.0, error ); commands.add(DisconnectClient { - client: message.client, + client: packet.client, reason: Text::translate(MULTIPLAYER_DISCONNECT_CHAT_VALIDATION_FAILED, []), }); continue; @@ -519,6 +653,15 @@ fn handle_message_events( continue; }; + let Ok(chat_session) = sessions.get(packet.client) else { + warn!("Player `{}` doesn't have a chat session", username.0); + commands.add(DisconnectClient { + client: packet.client, + reason: Text::translate(CHAT_DISABLED_MISSING_PROFILE_KEY, []) + }); + continue; + }; + // Verify that the player's session has not expired. if SystemTime::now() .duration_since(SystemTime::UNIX_EPOCH) @@ -528,22 +671,12 @@ fn handle_message_events( { warn!("Player `{}` has an expired chat session", username.0); commands.add(DisconnectClient { - client: message.client, + client: packet.client, reason: Text::translate(CHAT_DISABLED_EXPIRED_PROFILE_KEY, []), }); continue; } - // Verify that the chat message is signed. - let Some(message_signature) = &message.signature else { - warn!("Received unsigned chat message from `{}`", username.0); - commands.add(DisconnectClient { - client: message.client, - reason: Text::translate(MULTIPLAYER_DISCONNECT_UNSIGNED_CHAT, []) - }); - continue; - }; - // Create the hash digest used to verify the chat message. let mut hasher = Sha256::new_with_prefix([0u8, 0, 0, 1]); @@ -575,51 +708,70 @@ fn handle_message_events( { warn!("Failed to verify chat message from `{}`", username.0); commands.add(DisconnectClient { - client: message.client, + client: packet.client, reason: Text::translate(MULTIPLAYER_DISCONNECT_UNSIGNED_CHAT, []), }); continue; } - info!("{}: {}", username.0, message.message.as_ref()); + message_events.send(ChatMessage { + client: packet.client, + message: message.message.into(), + timestamp: message.timestamp, + message_type: ChatMessageType::Signed { + salt: message.salt, + signature: (*message_signature).into(), + message_index: link.index, + sender: link.sender, + last_seen, + }, + }); + } +} - let username = username.0.clone(); +#[cfg(not(feature = "secure"))] +fn handle_message_packets( + mut clients: Query<(&mut Client, &mut ChatState, &ClientSettings)>, + mut packets: EventReader, + mut message_events: EventWriter, +) { + for packet in packets.iter() { + let Some(message) = packet.decode::() else { + continue; + }; - // Broadcast the chat message to other clients. - for (mut client, mut state, ..) in clients.iter_mut() { - // Create a list of messages that have been seen by the client. - let previous = last_seen - .iter() - .map(|sig| match state.signature_storage.index_of(sig) { - Some(index) => MessageSignature::ByIndex(index), - None => MessageSignature::BySignature(sig), - }) - .collect::>(); + let Ok((mut client, mut state, settings)) = clients.get_mut(packet.client) else { + warn!("Unable to find client for message '{:?}'", message); + continue; + }; - client.write_packet(&ChatMessageS2c { - sender: link.sender, - index: link.index.into(), - message_signature: Some(message_signature.as_ref()), - message: message.message.as_ref(), - time_stamp: message.timestamp, - salt: message.salt, - previous_messages: previous, - unsigned_content: None, - filter_type: MessageFilterType::PassThrough, - chat_type: 0.into(), // TODO: Make chat type for player messages selectable - network_name: Text::from(username.clone()).into(), - network_target_name: None, - }); - // Add pending acknowledgement. - state.add_pending(&mut last_seen.clone(), *message_signature.as_ref()); - if state.validator.message_count() > 4096 { - warn!("User has too many pending chats `{}`", username); - commands.add(DisconnectClient { - client: message.client, - reason: Text::translate(MULTIPLAYER_DISCONNECT_TOO_MANY_PENDING_CHATS, []), - }); - continue; - } + // Ensure that the client isn't sending messages while their chat is hidden. + if settings.chat_mode == ChatMode::Hidden { + client.send_message(Text::translate(CHAT_DISABLED_OPTIONS, []).color(Color::RED)); + continue; + } + + state.last_message_timestamp = message.timestamp; + + message_events.send(ChatMessage { + client: packet.client, + message: message.message.into(), + timestamp: message.timestamp, + }) + } +} + +fn handle_command_packets( + mut packets: EventReader, + mut command_events: EventWriter, +) { + for packet in packets.iter() { + if let Some(command) = packet.decode::() { + command_events.send(CommandExecution { + client: packet.client, + command: command.command.into(), + timestamp: command.timestamp, + }) } } } diff --git a/crates/valence_client/Cargo.toml b/crates/valence_client/Cargo.toml index 8394490c9..5d50ac371 100644 --- a/crates/valence_client/Cargo.toml +++ b/crates/valence_client/Cargo.toml @@ -3,6 +3,10 @@ name = "valence_client" version.workspace = true edition.workspace = true +[features] +chat = [] +secure_chat = ["chat"] + [dependencies] anyhow.workspace = true bevy_app.workspace = true diff --git a/crates/valence_client/src/chat.rs b/crates/valence_client/src/chat.rs new file mode 100644 index 000000000..6b8fd5f73 --- /dev/null +++ b/crates/valence_client/src/chat.rs @@ -0,0 +1,32 @@ +use bevy_ecs::prelude::*; +#[cfg(feature = "secure_chat")] +use uuid::Uuid; + +#[derive(Clone, Debug)] +pub struct CommandExecution { + pub client: Entity, + pub command: Box, + pub timestamp: u64, +} + +#[derive(Clone, Debug)] +pub struct ChatMessage { + pub client: Entity, + pub message: Box, + pub timestamp: u64, + #[cfg(feature = "secure_chat")] + pub message_type: ChatMessageType, +} + +#[cfg(feature = "secure_chat")] +#[derive(Clone, Debug)] +pub enum ChatMessageType { + Signed { + salt: u64, + signature: Box<[u8; 256]>, + message_index: i32, + sender: Uuid, + last_seen: Vec<[u8; 256]>, + }, + Unsigned, +} diff --git a/crates/valence_client/src/lib.rs b/crates/valence_client/src/lib.rs index 37c6eb64f..f4b1d71c5 100644 --- a/crates/valence_client/src/lib.rs +++ b/crates/valence_client/src/lib.rs @@ -69,6 +69,8 @@ use valence_instance::{ClearInstanceChangesSet, Instance, WriteUpdatePacketsToIn use valence_registry::{RegistryCodec, RegistryCodecSet}; pub mod action; +#[cfg(feature = "chat")] +pub mod chat; pub mod command; pub mod event_loop; pub mod interact_entity; diff --git a/crates/valence_client/src/misc.rs b/crates/valence_client/src/misc.rs index d499fb465..fa73a930d 100644 --- a/crates/valence_client/src/misc.rs +++ b/crates/valence_client/src/misc.rs @@ -4,10 +4,9 @@ use glam::Vec3; use valence_core::block_pos::BlockPos; use valence_core::direction::Direction; use valence_core::hand::Hand; -use valence_core::packet::c2s::play::player_session::PlayerSessionData; use valence_core::packet::c2s::play::{ - ChatMessageC2s, ClientStatusC2s, CommandExecutionC2s, HandSwingC2s, MessageAcknowledgmentC2s, - PlayerInteractBlockC2s, PlayerInteractItemC2s, PlayerSessionC2s, ResourcePackStatusC2s, + ClientStatusC2s, HandSwingC2s, PlayerInteractBlockC2s, PlayerInteractItemC2s, + ResourcePackStatusC2s, }; use valence_entity::{EntityAnimation, EntityAnimations}; @@ -17,10 +16,6 @@ use crate::event_loop::{EventLoopSchedule, EventLoopSet, PacketEvent}; pub(super) fn build(app: &mut App) { app.add_event::() .add_event::() - .add_event::() - .add_event::() - .add_event::() - .add_event::() .add_event::() .add_event::() .add_event::() @@ -54,36 +49,6 @@ pub struct InteractBlock { pub sequence: i32, } -#[derive(Clone, Debug)] -pub struct CommandExecution { - pub client: Entity, - pub command: Box, - pub timestamp: u64, -} - -#[derive(Clone, Debug)] -pub struct ChatMessage { - pub client: Entity, - pub message: Box, - pub timestamp: u64, - pub salt: u64, - pub signature: Option>, - pub message_index: i32, - pub acknowledgements: [u8; 3], -} - -#[derive(Copy, Clone, Debug)] -pub struct MessageAcknowledgment { - pub client: Entity, - pub message_index: i32, -} - -#[derive(Clone, Debug)] -pub struct PlayerSession { - pub client: Entity, - pub session_data: PlayerSessionData, -} - #[derive(Copy, Clone, Debug)] pub struct Respawn { pub client: Entity, @@ -118,10 +83,6 @@ fn handle_misc_packets( mut clients: Query<(&mut ActionSequence, &mut EntityAnimations)>, mut hand_swing_events: EventWriter, mut interact_block_events: EventWriter, - mut command_execution_events: EventWriter, - mut chat_message_events: EventWriter, - mut message_acknowledgement_events: EventWriter, - mut player_session_events: EventWriter, mut respawn_events: EventWriter, mut request_stats_events: EventWriter, mut resource_pack_status_change_events: EventWriter, @@ -159,32 +120,6 @@ fn handle_misc_packets( } // TODO - } else if let Some(pkt) = packet.decode::() { - command_execution_events.send(CommandExecution { - client: packet.client, - command: pkt.command.into(), - timestamp: pkt.timestamp, - }); - } else if let Some(pkt) = packet.decode::() { - chat_message_events.send(ChatMessage { - client: packet.client, - message: pkt.message.into(), - timestamp: pkt.timestamp, - salt: pkt.salt, - signature: pkt.signature.copied().map(Box::new), - message_index: pkt.message_index.0, - acknowledgements: pkt.acknowledgement, - }); - } else if let Some(pkt) = packet.decode::() { - message_acknowledgement_events.send(MessageAcknowledgment { - client: packet.client, - message_index: pkt.message_index.0, - }); - } else if let Some(pkt) = packet.decode::() { - player_session_events.send(PlayerSession { - client: packet.client, - session_data: pkt.0.into_owned(), - }); } else if let Some(pkt) = packet.decode::() { match pkt { ClientStatusC2s::PerformRespawn => respawn_events.send(Respawn { From 87ec95b9062c048847132e2c39f37051f2534d5b Mon Sep 17 00:00:00 2001 From: Guac Date: Mon, 25 Sep 2023 11:01:58 -0600 Subject: [PATCH 20/32] Remove old files --- crates/valence_server/src/chat.rs | 32 ---------- examples/chat.rs | 97 ------------------------------- 2 files changed, 129 deletions(-) delete mode 100644 crates/valence_server/src/chat.rs delete mode 100644 examples/chat.rs diff --git a/crates/valence_server/src/chat.rs b/crates/valence_server/src/chat.rs deleted file mode 100644 index 6b8fd5f73..000000000 --- a/crates/valence_server/src/chat.rs +++ /dev/null @@ -1,32 +0,0 @@ -use bevy_ecs::prelude::*; -#[cfg(feature = "secure_chat")] -use uuid::Uuid; - -#[derive(Clone, Debug)] -pub struct CommandExecution { - pub client: Entity, - pub command: Box, - pub timestamp: u64, -} - -#[derive(Clone, Debug)] -pub struct ChatMessage { - pub client: Entity, - pub message: Box, - pub timestamp: u64, - #[cfg(feature = "secure_chat")] - pub message_type: ChatMessageType, -} - -#[cfg(feature = "secure_chat")] -#[derive(Clone, Debug)] -pub enum ChatMessageType { - Signed { - salt: u64, - signature: Box<[u8; 256]>, - message_index: i32, - sender: Uuid, - last_seen: Vec<[u8; 256]>, - }, - Unsigned, -} diff --git a/examples/chat.rs b/examples/chat.rs deleted file mode 100644 index a11c94f82..000000000 --- a/examples/chat.rs +++ /dev/null @@ -1,97 +0,0 @@ -#![allow(clippy::type_complexity)] - -use tracing::warn; -use valence::chat::ChatState; -use valence::client::chat::{ChatMessage, CommandExecution}; -use valence::client::despawn_disconnected_clients; -use valence::prelude::*; - -const SPAWN_Y: i32 = 64; - -pub fn main() { - tracing_subscriber::fmt().init(); - - App::new() - .add_plugins(DefaultPlugins) - .add_startup_system(setup) - .add_systems(( - init_clients, - despawn_disconnected_clients, - handle_message_events.in_schedule(EventLoopSchedule), - handle_command_events.in_schedule(EventLoopSchedule), - )) - .run(); -} - -fn setup( - mut commands: Commands, - server: Res, - dimensions: Query<&DimensionType>, - biomes: Query<&Biome>, -) { - let mut instance = Instance::new(ident!("overworld"), &dimensions, &biomes, &server); - - for z in -5..5 { - for x in -5..5 { - instance.insert_chunk([x, z], Chunk::default()); - } - } - - for z in -25..25 { - for x in -25..25 { - instance.set_block([x, SPAWN_Y, z], BlockState::GRASS_BLOCK); - } - } - - commands.spawn(instance); -} - -fn init_clients( - mut clients: Query<(&mut Client, &mut Location, &mut Position, &mut GameMode), Added>, - instances: Query>, -) { - for (mut client, mut loc, mut pos, mut game_mode) in &mut clients { - *game_mode = GameMode::Creative; - loc.0 = instances.single(); - pos.set([0.0, SPAWN_Y as f64 + 1.0, 0.0]); - - client.send_message("Welcome to Valence! Say something.".italic()); - } -} - -fn handle_message_events( - mut clients: Query<(&mut Client, &mut ChatState)>, - names: Query<&Username>, - mut messages: EventReader, -) { - for message in messages.iter() { - let sender_name = names.get(message.client).expect("Error getting username"); - // Need to find better way. Username is sender, while client and chat state are - // recievers. Maybe try to add a chat feature to Client. - for (mut client, mut state) in clients.iter_mut() { - state - .as_mut() - .send_chat_message(client.as_mut(), sender_name, message) - .expect("Error sending message"); - } - } -} - -fn handle_command_events( - mut clients: Query<&mut Client>, - mut commands: EventReader, -) { - for command in commands.iter() { - let Ok(mut client) = clients.get_component_mut::(command.client) else { - warn!("Unable to find client for message: {:?}", command); - continue; - }; - - let message = command.command.to_string(); - - let formatted = - "You sent the command ".into_text() + ("/".into_text() + (message).into_text()).bold(); - - client.send_message(formatted); - } -} From 36df78ac8080f8cb08bae9cfa5c03f29bdfdbc30 Mon Sep 17 00:00:00 2001 From: Guac Date: Fri, 29 Sep 2023 17:09:08 -0600 Subject: [PATCH 21/32] Major refactoring Just updated the PR to the project --- Cargo.toml | 6 + assets/depgraph.svg | 373 +++++++++--------- crates/valence_chat/Cargo.toml | 12 +- crates/valence_chat/src/lib.rs | 139 +++---- crates/valence_chat/src/message.rs | 65 +++ crates/valence_player_list/Cargo.toml | 1 + crates/valence_player_list/src/lib.rs | 32 +- .../src/packets/play/chat_message_s2c.rs | 106 +---- .../play/message_acknowledgment_c2s.rs | 2 +- .../src/packets/play/player_list_s2c.rs | 12 +- .../src/packets/play/player_session_c2s.rs | 27 +- crates/valence_registry/Cargo.toml | 1 + crates/valence_registry/src/chat_type.rs | 240 +++++++++++ crates/valence_registry/src/lib.rs | 1 + crates/valence_server/src/lib.rs | 1 - crates/valence_server/src/message.rs | 63 --- examples/anvil_loading.rs | 4 +- examples/block_entities.rs | 2 +- examples/boss_bar.rs | 14 +- examples/building.rs | 2 +- examples/chat.rs | 115 ++++++ examples/cow_sphere.rs | 2 +- examples/ctf.rs | 12 +- examples/death.rs | 2 +- examples/entity_hitbox.rs | 2 +- examples/game_of_life.rs | 4 +- examples/parkour.rs | 4 +- examples/player_list.rs | 2 +- examples/resource_pack.rs | 12 +- examples/text.rs | 54 +-- examples/world_border.rs | 6 +- src/lib.rs | 12 +- 32 files changed, 830 insertions(+), 500 deletions(-) create mode 100644 crates/valence_chat/src/message.rs create mode 100644 crates/valence_registry/src/chat_type.rs delete mode 100644 crates/valence_server/src/message.rs create mode 100644 examples/chat.rs diff --git a/Cargo.toml b/Cargo.toml index cccade60f..ebc921e8c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,6 +24,8 @@ default = [ "world_border", "weather", "testing", + "chat", + "secure_chat", ] advancement = ["dep:valence_advancement"] anvil = ["dep:valence_anvil"] @@ -35,6 +37,8 @@ player_list = ["dep:valence_player_list"] scoreboard = ["dep:valence_scoreboard"] world_border = ["dep:valence_world_border"] weather = ["dep:valence_weather"] +chat = ["dep:valence_chat"] +secure_chat = ["chat", "valence_chat?/secure"] testing = [] [dependencies] @@ -61,6 +65,7 @@ valence_world_border = { workspace = true, optional = true } valence_lang.workspace = true valence_text.workspace = true valence_ident.workspace = true +valence_chat = { workspace = true, optional = true } [dev-dependencies] anyhow.workspace = true @@ -174,6 +179,7 @@ valence_advancement = { path = "crates/valence_advancement", version = "0.2.0-al valence_anvil = { path = "crates/valence_anvil", version = "0.2.0-alpha.1" } valence_boss_bar = { path = "crates/valence_boss_bar", version = "0.2.0-alpha.1" } valence_build_utils = { path = "crates/valence_build_utils", version = "0.2.0-alpha.1" } +valence_chat = { path = "crates/valence_chat", version = "0.2.0-alpha.1" } valence_entity = { path = "crates/valence_entity", version = "0.2.0-alpha.1" } valence_generated = { path = "crates/valence_generated", version = "0.2.0-alpha.1" } valence_ident = { path = "crates/valence_ident", version = "0.2.0-alpha.1" } diff --git a/assets/depgraph.svg b/assets/depgraph.svg index b9523f69e..892e1d726 100644 --- a/assets/depgraph.svg +++ b/assets/depgraph.svg @@ -1,379 +1,396 @@ - - - - -%3 - + + + + 0 - -valence_advancement + +valence_advancement 1 - -valence_server + +valence_server 0->1 - - + + 2 - -valence_entity + +valence_entity 1->2 - - + + 11 - -valence_registry + +valence_registry 1->11 - - + + 10 - -valence_server_common + +valence_server_common 2->10 - - + + 11->10 - - + + 6 - -valence_protocol + +valence_protocol 10->6 - - + + 3 - -valence_math + +valence_math 4 - -valence_nbt + +valence_nbt 5 - -valence_ident + +valence_ident 7 - -valence_generated + +valence_generated 6->7 - - + + 9 - -valence_text + +valence_text 6->9 - - + + 7->3 - - + + 7->5 - - + + 9->4 - - + + 9->5 - - + + 8 - -valence_build_utils + +valence_build_utils 12 - -valence_anvil + +valence_anvil 12->1 - - + + 13 - -valence_boss_bar + +valence_boss_bar 13->1 - - + + 14 - -valence_inventory - - - -14->1 - - + +valence_chat 15 - -valence_lang + +valence_lang + + + +14->15 + + 16 - -valence_network + +valence_player_list - + -16->1 - - +14->16 + + - + -16->15 - - +16->1 + + 17 - -valence_player_list + +valence_inventory 17->1 - - + + 18 - -valence_scoreboard + +valence_network 18->1 - - + + + + + +18->15 + + 19 - -valence_spatial + +valence_scoreboard + + + +19->1 + + 20 - -valence_weather - - - -20->1 - - + +valence_spatial 21 - -valence_world_border + +valence_weather - + 21->1 - - + + 22 - -dump_schedule + +valence_world_border + + + +22->1 + + 23 - -valence - - - -22->23 - - + +dump_schedule - - -23->0 - - + + +24 + +valence - + -23->12 - - +23->24 + + - + -23->13 - - +24->0 + + - + -23->14 - - +24->12 + + - + -23->16 - - +24->13 + + - + -23->17 - - +24->14 + + - + -23->18 - - +24->17 + + - + -23->20 - - +24->18 + + - + -23->21 - - +24->19 + + - - -24 - -packet_inspector - - + -24->6 - - +24->21 + + + + + +24->22 + + 25 - -playground + +packet_inspector - - -25->23 - - + + +25->6 + + 26 - -stresser - - - -26->6 - - + +playground + + + +26->24 + + + + + +27 + +stresser + + + +27->6 + + diff --git a/crates/valence_chat/Cargo.toml b/crates/valence_chat/Cargo.toml index ee66a3b51..cfeaa3903 100644 --- a/crates/valence_chat/Cargo.toml +++ b/crates/valence_chat/Cargo.toml @@ -4,7 +4,7 @@ version.workspace = true edition.workspace = true [features] -secure = ["dep:rsa", "dep:rustc-hash", "dep:sha1", "dep:sha2", "valence_client/secure_chat"] +secure = ["dep:rsa", "dep:rustc-hash", "dep:sha1", "dep:sha2"] [dependencies] anyhow.workspace = true @@ -15,10 +15,12 @@ rustc-hash = { workspace = true, optional = true } sha1 = { workspace = true, features = ["oid"], optional = true } sha2 = { workspace = true, features = ["oid"], optional = true } tracing.workspace = true -valence_core.workspace = true -valence_client = { workspace = true, features = ["chat"], optional = true } -valence_instance.workspace = true +uuid.workspace = true +valence_lang.workspace = true valence_nbt.workspace = true valence_player_list.workspace = true +valence_protocol.workspace = true valence_registry.workspace = true -uuid.workspace = true \ No newline at end of file +valence_server.workspace = true +valence_server_common.workspace = true +valence_text.workspace = true diff --git a/crates/valence_chat/src/lib.rs b/crates/valence_chat/src/lib.rs index 00ba27a8e..5cc0d863f 100644 --- a/crates/valence_chat/src/lib.rs +++ b/crates/valence_chat/src/lib.rs @@ -17,7 +17,7 @@ clippy::dbg_macro )] -pub mod chat_type; +pub mod message; #[cfg(feature = "secure")] use std::collections::VecDeque; @@ -25,33 +25,33 @@ use std::time::SystemTime; use bevy_app::prelude::*; use bevy_ecs::prelude::*; -use chat_type::ChatTypePlugin; use tracing::warn; -use valence_client::chat::{ChatMessage, CommandExecution}; -use valence_client::event_loop::{EventLoopSchedule, EventLoopSet, PacketEvent, RunEventLoopSet}; -use valence_client::settings::ClientSettings; -use valence_client::{Client, SpawnClientsSet}; -use valence_core::packet::c2s::play::client_settings::ChatMode; -use valence_core::packet::c2s::play::{ChatMessageC2s, CommandExecutionC2s}; -use valence_core::packet::encode::WritePacket; -use valence_core::packet::message_signature::MessageSignature; -use valence_core::packet::s2c::play::chat_message::MessageFilterType; -use valence_core::packet::s2c::play::{ChatMessageS2c, ProfilelessChatMessageS2c}; -use valence_core::text::{Color, Text, TextFormat}; -use valence_core::translation_key::{CHAT_DISABLED_OPTIONS, DISCONNECT_GENERIC_REASON}; + +use valence_registry::chat_type::ChatTypePlugin; +use valence_server::event_loop::{EventLoopPreUpdate, PacketEvent}; +use valence_server::client::{Client, SpawnClientsSet}; +use valence_server::client_settings::ClientSettings; +use valence_protocol::packets::play::client_settings_c2s::ChatMode; +use valence_protocol::packets::play::{ChatMessageC2s, CommandExecutionC2s}; +use valence_server::protocol::WritePacket; +use valence_server::protocol::packets::play::chat_message_s2c::{MessageFilterType, MessageSignature}; +use valence_server::protocol::packets::play::{ChatMessageS2c, ProfilelessChatMessageS2c}; +use valence_text::{Color, Text}; +use valence_lang::keys::{CHAT_DISABLED_OPTIONS, DISCONNECT_GENERIC_REASON}; #[cfg(feature = "secure")] use { anyhow::bail, + rsa::pkcs1v15::Pkcs1v15Sign, rsa::pkcs8::DecodePublicKey, - rsa::{PaddingScheme, PublicKey, RsaPublicKey}, + rsa::RsaPublicKey, rustc_hash::{FxHashMap, FxHashSet}, sha1::{Digest, Sha1}, sha2::Sha256, uuid::Uuid, - valence_client::chat::ChatMessageType, - valence_client::{DisconnectClient, Username}, - valence_core::packet::c2s::play::{MessageAcknowledgmentC2s, PlayerSessionC2s}, - valence_core::translation_key::{ + valence_server::client::{DisconnectClient, Username}, + valence_server::protocol::packets::play::{MessageAcknowledgmentC2s, PlayerSessionC2s}, + valence_text::IntoText, + valence_lang::keys::{ CHAT_DISABLED_CHAIN_BROKEN, CHAT_DISABLED_EXPIRED_PROFILE_KEY, CHAT_DISABLED_MISSING_PROFILE_KEY, MULTIPLAYER_DISCONNECT_CHAT_VALIDATION_FAILED, MULTIPLAYER_DISCONNECT_EXPIRED_PUBLIC_KEY, @@ -59,10 +59,12 @@ use { MULTIPLAYER_DISCONNECT_OUT_OF_ORDER_CHAT, MULTIPLAYER_DISCONNECT_TOO_MANY_PENDING_CHATS, MULTIPLAYER_DISCONNECT_UNSIGNED_CHAT, }, - valence_core::uuid::UniqueId, + valence_server_common::UniqueId, valence_player_list::{ChatSession, PlayerListEntry}, }; +use crate::message::{ChatMessageEvent, ChatMessageType, CommandExecutionEvent, SendMessage}; + #[cfg(feature = "secure")] const MOJANG_KEY_DATA: &[u8] = include_bytes!("../yggdrasil_session_pubkey.der"); @@ -70,25 +72,20 @@ pub struct ChatPlugin; impl Plugin for ChatPlugin { fn build(&self, app: &mut bevy_app::App) { - app.add_plugin(ChatTypePlugin) - .add_event::() - .add_event::() - .add_system( - init_chat_states - .in_base_set(CoreSet::PreUpdate) - .after(SpawnClientsSet) - .before(RunEventLoopSet), + app.add_plugins(ChatTypePlugin) + .add_systems( + PreUpdate, + init_chat_states.after(SpawnClientsSet), ) .add_systems( + EventLoopPreUpdate, ( #[cfg(feature = "secure")] handle_acknowledgement_packets, #[cfg(not(feature = "secure"))] handle_message_packets, handle_command_packets, - ) - .in_base_set(EventLoopSet::PreUpdate) - .in_schedule(EventLoopSchedule), + ), ); #[cfg(feature = "secure")] @@ -98,12 +95,12 @@ impl Plugin for ChatPlugin { app.insert_resource(MojangServicesState::new(mojang_pub_key)) .add_systems( - (handle_session_packets, handle_message_packets) - .chain() - .in_base_set(EventLoopSet::PreUpdate) - .in_schedule(EventLoopSchedule), + EventLoopPreUpdate, + (handle_session_packets, handle_message_packets).chain(), ); } + + message::build(app); } } @@ -120,17 +117,16 @@ impl MojangServicesState { } } +#[cfg(feature = "secure")] #[derive(Debug, Component)] pub struct ChatState { pub last_message_timestamp: u64, - #[cfg(feature = "secure")] validator: AcknowledgementValidator, - #[cfg(feature = "secure")] chain: MessageChain, - #[cfg(feature = "secure")] signature_storage: MessageSignatureStorage, } +#[cfg(feature = "secure")] impl Default for ChatState { fn default() -> Self { Self { @@ -138,31 +134,18 @@ impl Default for ChatState { .duration_since(SystemTime::UNIX_EPOCH) .expect("Unable to get Unix time") .as_millis() as u64, - #[cfg(feature = "secure")] validator: AcknowledgementValidator::new(), - #[cfg(feature = "secure")] chain: MessageChain::new(), - #[cfg(feature = "secure")] signature_storage: MessageSignatureStorage::new(), } } } +#[cfg(feature = "secure")] impl ChatState { - pub fn send_chat_message( - &mut self, - client: &mut Client, - username: &Username, - message: &ChatMessage, - ) -> anyhow::Result<()> { + pub fn send_chat_message(&mut self, client: &mut Client, username: &Username, message: &ChatMessageEvent) -> anyhow::Result<()> { match &message.message_type { - ChatMessageType::Signed { - salt, - signature, - message_index, - last_seen, - sender, - } => { + ChatMessageType::Signed { salt, signature, message_index, last_seen, sender } => { // Create a list of messages that have been seen by the client. let previous = last_seen .iter() @@ -176,8 +159,8 @@ impl ChatState { sender: *sender, index: (*message_index).into(), message_signature: Some((*signature).as_ref()), - message: message.message.as_ref(), - time_stamp: message.timestamp, + message: message.message.as_ref().into(), + timestamp: message.timestamp, salt: *salt, previous_messages: previous, unsigned_content: None, @@ -205,7 +188,6 @@ impl ChatState { /// Updates the chat state's previously seen signatures with a new one /// `signature`. - #[cfg(feature = "secure")] fn add_pending(&mut self, last_seen: &Vec<[u8; 256]>, signature: &[u8; 256]) { self.signature_storage.add(last_seen, signature); self.validator.add_pending(signature); @@ -438,6 +420,7 @@ impl MessageSignatureStorage { } } +#[cfg(feature = "secure")] fn init_chat_states(clients: Query>, mut commands: Commands) { for entity in clients.iter() { commands.entity(entity).insert(ChatState::default()); @@ -488,7 +471,7 @@ fn handle_session_packets( if services_state .public_key .verify( - PaddingScheme::new_pkcs1v15_sign::(), + Pkcs1v15Sign::new::(), // PaddingScheme::new_pkcs1v15_sign::(), &hash, session.0.key_signature.as_ref(), ) @@ -575,7 +558,7 @@ fn handle_message_packets( >, sessions: Query<&ChatSession, With>, mut packets: EventReader, - mut message_events: EventWriter, + mut message_events: EventWriter, mut commands: Commands, ) { for packet in packets.iter() { @@ -590,7 +573,7 @@ fn handle_message_packets( // Ensure that the client isn't sending messages while their chat is hidden. if settings.chat_mode == ChatMode::Hidden { - client.send_message(Text::translate(CHAT_DISABLED_OPTIONS, []).color(Color::RED)); + client.send_game_message(Text::translate(CHAT_DISABLED_OPTIONS, []).color(Color::RED)); continue; } @@ -617,11 +600,11 @@ fn handle_message_packets( client: packet.client, reason: Text::translate(MULTIPLAYER_DISCONNECT_UNSIGNED_CHAT, []) });*/ - message_events.send(ChatMessage { + message_events.send(ChatMessageEvent { client: packet.client, - message: message.message.into(), + message: message.message.0.into(), timestamp: message.timestamp, - message_type: ChatMessageType::Unsigned, + message_type: message::ChatMessageType::Unsigned, }); continue; }; @@ -629,7 +612,7 @@ fn handle_message_packets( // Validate the message acknowledgements. let last_seen = match state .validator - .validate(&message.acknowledgement, message.message_index.0) + .validate(&message.acknowledgement.0, message.message_index.0) { Err(error) => { warn!( @@ -646,7 +629,7 @@ fn handle_message_packets( }; let Some(link) = &state.chain.next_link() else { - client.send_message(Text::translate( + client.send_game_message(Text::translate( CHAT_DISABLED_CHAIN_BROKEN, [], ).color(Color::RED)); @@ -700,7 +683,7 @@ fn handle_message_packets( if chat_session .public_key .verify( - PaddingScheme::new_pkcs1v15_sign::(), + Pkcs1v15Sign::new::(), // PaddingScheme::new_pkcs1v15_sign::(), &hashed, message_signature.as_ref(), ) @@ -714,11 +697,11 @@ fn handle_message_packets( continue; } - message_events.send(ChatMessage { + message_events.send(ChatMessageEvent { client: packet.client, - message: message.message.into(), + message: message.message.0.into(), timestamp: message.timestamp, - message_type: ChatMessageType::Signed { + message_type: message::ChatMessageType::Signed { salt: message.salt, signature: (*message_signature).into(), message_index: link.index, @@ -731,29 +714,27 @@ fn handle_message_packets( #[cfg(not(feature = "secure"))] fn handle_message_packets( - mut clients: Query<(&mut Client, &mut ChatState, &ClientSettings)>, + mut clients: Query<(&mut Client, &ClientSettings)>, mut packets: EventReader, - mut message_events: EventWriter, + mut message_events: EventWriter, ) { for packet in packets.iter() { let Some(message) = packet.decode::() else { continue; }; - let Ok((mut client, mut state, settings)) = clients.get_mut(packet.client) else { + let Ok((mut client, settings)) = clients.get_mut(packet.client) else { warn!("Unable to find client for message '{:?}'", message); continue; }; // Ensure that the client isn't sending messages while their chat is hidden. if settings.chat_mode == ChatMode::Hidden { - client.send_message(Text::translate(CHAT_DISABLED_OPTIONS, []).color(Color::RED)); + client.send_game_message(Text::translate(CHAT_DISABLED_OPTIONS, []).color(Color::RED)); continue; } - state.last_message_timestamp = message.timestamp; - - message_events.send(ChatMessage { + message_events.send(ChatMessageEvent { client: packet.client, message: message.message.into(), timestamp: message.timestamp, @@ -763,13 +744,13 @@ fn handle_message_packets( fn handle_command_packets( mut packets: EventReader, - mut command_events: EventWriter, + mut command_events: EventWriter, ) { for packet in packets.iter() { if let Some(command) = packet.decode::() { - command_events.send(CommandExecution { + command_events.send(CommandExecutionEvent { client: packet.client, - command: command.command.into(), + command: command.command.0.into(), timestamp: command.timestamp, }) } diff --git a/crates/valence_chat/src/message.rs b/crates/valence_chat/src/message.rs new file mode 100644 index 000000000..1d68a7247 --- /dev/null +++ b/crates/valence_chat/src/message.rs @@ -0,0 +1,65 @@ +use bevy_app::prelude::*; +use bevy_ecs::prelude::*; +use valence_protocol::encode::WritePacket; +use valence_protocol::packets::play::GameMessageS2c; +use valence_protocol::text::IntoText; + +#[cfg(feature = "secure")] +use uuid::Uuid; + +pub(super) fn build(app: &mut App) { + app.add_event::() + .add_event::(); +} + +pub trait SendMessage { + /// Sends a system message visible in the chat. + fn send_game_message<'a>(&mut self, msg: impl IntoText<'a>); + /// Displays a message in the player's action bar (text above the hotbar). + fn send_action_bar_message<'a>(&mut self, msg: impl IntoText<'a>); +} + +impl SendMessage for T { + fn send_game_message<'a>(&mut self, msg: impl IntoText<'a>) { + self.write_packet(&GameMessageS2c { + chat: msg.into_cow_text(), + overlay: false, + }); + } + + fn send_action_bar_message<'a>(&mut self, msg: impl IntoText<'a>) { + self.write_packet(&GameMessageS2c { + chat: msg.into_cow_text(), + overlay: true, + }); + } +} + +#[derive(Event, Clone, Debug)] +pub struct CommandExecutionEvent { + pub client: Entity, + pub command: Box, + pub timestamp: u64, +} + +#[derive(Event, Clone, Debug)] +pub struct ChatMessageEvent { + pub client: Entity, + pub message: Box, + pub timestamp: u64, + #[cfg(feature = "secure")] + pub message_type: ChatMessageType, +} + +#[cfg(feature = "secure")] +#[derive(Clone, Debug)] +pub enum ChatMessageType { + Signed { + salt: u64, + signature: Box<[u8; 256]>, + message_index: i32, + sender: Uuid, + last_seen: Vec<[u8; 256]>, + }, + Unsigned, +} \ No newline at end of file diff --git a/crates/valence_player_list/Cargo.toml b/crates/valence_player_list/Cargo.toml index 176c94389..2fe9d3784 100644 --- a/crates/valence_player_list/Cargo.toml +++ b/crates/valence_player_list/Cargo.toml @@ -13,4 +13,5 @@ bevy_app.workspace = true bevy_ecs.workspace = true bitfield-struct.workspace = true derive_more.workspace = true +rsa.workspace = true valence_server.workspace = true diff --git a/crates/valence_player_list/src/lib.rs b/crates/valence_player_list/src/lib.rs index db42f68b5..0d0ed31da 100644 --- a/crates/valence_player_list/src/lib.rs +++ b/crates/valence_player_list/src/lib.rs @@ -23,6 +23,7 @@ use std::borrow::Cow; use bevy_app::prelude::*; use bevy_ecs::prelude::*; use derive_more::{Deref, DerefMut}; +use rsa::RsaPublicKey; use valence_server::client::{Client, Properties, Username}; use valence_server::keepalive::Ping; use valence_server::layer::UpdateLayersPreClientSet; @@ -30,6 +31,7 @@ use valence_server::protocol::encode::PacketWriter; use valence_server::protocol::packets::play::{ player_list_s2c as packet, PlayerListHeaderS2c, PlayerListS2c, PlayerRemoveS2c, }; +use valence_server::protocol::packets::play::player_session_c2s::PlayerSessionData; use valence_server::protocol::WritePacket; use valence_server::text::IntoText; use valence_server::uuid::Uuid; @@ -154,6 +156,14 @@ impl Default for Listed { } } +/// Contains information for the player's chat message verification. +/// Not required. +#[derive(Component, Clone, Debug)] +pub struct ChatSession { + pub public_key: RsaPublicKey, + pub session_data: PlayerSessionData, +} + fn update_header_footer(player_list: ResMut, server: Res) { if player_list.changed_header_or_footer { let player_list = player_list.into_inner(); @@ -200,6 +210,7 @@ fn init_player_list_for_clients( &Ping, &DisplayName, &Listed, + Option<&ChatSession>, ), With, >, @@ -211,17 +222,18 @@ fn init_player_list_for_clients( .with_update_game_mode(true) .with_update_listed(true) .with_update_latency(true) - .with_update_display_name(true); + .with_update_display_name(true) + .with_initialize_chat(true); let entries: Vec<_> = entries .iter() .map( - |(uuid, username, props, game_mode, ping, display_name, listed)| { + |(uuid, username, props, game_mode, ping, display_name, listed, chat_session, )| { packet::PlayerListEntry { player_uuid: uuid.0, username: &username.0, properties: Cow::Borrowed(&props.0), - chat_data: None, + chat_data: chat_session.map(|s| s.session_data.clone().into()), listed: listed.0, ping: ping.0, game_mode: *game_mode, @@ -286,6 +298,7 @@ fn update_entries( Ref, Ref, Ref, + Option>, ), ( With, @@ -297,6 +310,7 @@ fn update_entries( Changed, Changed, Changed, + Changed, )>, ), >, @@ -310,7 +324,7 @@ fn update_entries( server.compression_threshold(), ); - for (uuid, username, props, game_mode, ping, display_name, listed) in &entries { + for (uuid, username, props, game_mode, ping, display_name, listed, chat_session) in &entries { let mut actions = packet::PlayerListActions::new(); // Did a change occur that would force us to overwrite the entry? This also adds @@ -333,6 +347,10 @@ fn update_entries( if listed.0 { actions.set_update_listed(true); } + + if chat_session.is_some() { + actions.set_initialize_chat(true); + } } else { if game_mode.is_changed() { actions.set_update_game_mode(true); @@ -350,6 +368,10 @@ fn update_entries( actions.set_update_listed(true); } + if matches!(&chat_session, Some(session) if session.is_changed()) { + actions.set_initialize_chat(true); + } + debug_assert_ne!(u8::from(actions), 0); } @@ -357,7 +379,7 @@ fn update_entries( player_uuid: uuid.0, username: &username.0, properties: Cow::Borrowed(&props.0), - chat_data: None, + chat_data: chat_session.map(|s| s.session_data.clone().into()), listed: listed.0, ping: ping.0, game_mode: *game_mode, diff --git a/crates/valence_protocol/src/packets/play/chat_message_s2c.rs b/crates/valence_protocol/src/packets/play/chat_message_s2c.rs index 99ccf2578..92b3d4858 100644 --- a/crates/valence_protocol/src/packets/play/chat_message_s2c.rs +++ b/crates/valence_protocol/src/packets/play/chat_message_s2c.rs @@ -6,7 +6,7 @@ use valence_text::Text; use crate::{Bounded, Decode, Encode, Packet, VarInt}; -#[derive(Clone, PartialEq, Debug, Packet)] +#[derive(Clone, PartialEq, Debug, Encode, Decode, Packet)] pub struct ChatMessageS2c<'a> { pub sender: Uuid, pub index: VarInt, @@ -17,99 +17,32 @@ pub struct ChatMessageS2c<'a> { pub previous_messages: Vec>, pub unsigned_content: Option>, pub filter_type: MessageFilterType, - pub filter_type_bits: Option, pub chat_type: VarInt, pub network_name: Cow<'a, Text>, pub network_target_name: Option>, } -#[derive(Copy, Clone, PartialEq, Eq, Debug, Encode, Decode)] +#[derive(Clone, PartialEq, Eq, Debug, Encode, Decode)] pub enum MessageFilterType { PassThrough, FullyFiltered, - PartiallyFiltered, -} - -impl<'a> Encode for ChatMessageS2c<'a> { - fn encode(&self, mut w: impl Write) -> anyhow::Result<()> { - self.sender.encode(&mut w)?; - self.index.encode(&mut w)?; - self.message_signature.encode(&mut w)?; - self.message.encode(&mut w)?; - self.timestamp.encode(&mut w)?; - self.salt.encode(&mut w)?; - self.previous_messages.encode(&mut w)?; - self.unsigned_content.encode(&mut w)?; - self.filter_type.encode(&mut w)?; - - if self.filter_type == MessageFilterType::PartiallyFiltered { - match self.filter_type_bits { - // Filler data - None => 0u8.encode(&mut w)?, - Some(bits) => bits.encode(&mut w)?, - } - } - - self.chat_type.encode(&mut w)?; - self.network_name.encode(&mut w)?; - self.network_target_name.encode(&mut w)?; - - Ok(()) - } -} - -impl<'a> Decode<'a> for ChatMessageS2c<'a> { - fn decode(r: &mut &'a [u8]) -> anyhow::Result { - let sender = Uuid::decode(r)?; - let index = VarInt::decode(r)?; - let message_signature = Option::<&'a [u8; 256]>::decode(r)?; - let message = Decode::decode(r)?; - let time_stamp = u64::decode(r)?; - let salt = u64::decode(r)?; - let previous_messages = Vec::::decode(r)?; - let unsigned_content = Option::>::decode(r)?; - let filter_type = MessageFilterType::decode(r)?; - - let filter_type_bits = match filter_type { - MessageFilterType::PartiallyFiltered => Some(u8::decode(r)?), - _ => None, - }; - - let chat_type = VarInt::decode(r)?; - let network_name = >::decode(r)?; - let network_target_name = Option::>::decode(r)?; - - Ok(Self { - sender, - index, - message_signature, - message, - timestamp: time_stamp, - salt, - previous_messages, - unsigned_content, - filter_type, - filter_type_bits, - chat_type, - network_name, - network_target_name, - }) - } + PartiallyFiltered { mask: Vec }, } #[derive(Copy, Clone, PartialEq, Debug)] -pub struct MessageSignature<'a> { - pub message_id: i32, - pub signature: Option<&'a [u8; 256]>, +pub enum MessageSignature<'a> { + ByIndex(i32), + BySignature(&'a [u8; 256]), } impl<'a> Encode for MessageSignature<'a> { fn encode(&self, mut w: impl Write) -> anyhow::Result<()> { - VarInt(self.message_id + 1).encode(&mut w)?; - - match self.signature { - None => {} - Some(signature) => signature.encode(&mut w)?, + match self { + MessageSignature::ByIndex(index) => VarInt(index + 1).encode(&mut w)?, + MessageSignature::BySignature(signature) => { + VarInt(0).encode(&mut w)?; + signature.encode(&mut w)?; + } } Ok(()) @@ -118,17 +51,12 @@ impl<'a> Encode for MessageSignature<'a> { impl<'a> Decode<'a> for MessageSignature<'a> { fn decode(r: &mut &'a [u8]) -> anyhow::Result { - let message_id = VarInt::decode(r)?.0 - 1; // TODO: this can underflow. + let index = VarInt::decode(r)?.0.saturating_sub(1); - let signature = if message_id == -1 { - Some(<&[u8; 256]>::decode(r)?) + if index == -1 { + Ok(MessageSignature::BySignature(<&[u8; 256]>::decode(r)?)) } else { - None - }; - - Ok(Self { - message_id, - signature, - }) + Ok(MessageSignature::ByIndex(index)) + } } } diff --git a/crates/valence_protocol/src/packets/play/message_acknowledgment_c2s.rs b/crates/valence_protocol/src/packets/play/message_acknowledgment_c2s.rs index f490751be..15a0d17d0 100644 --- a/crates/valence_protocol/src/packets/play/message_acknowledgment_c2s.rs +++ b/crates/valence_protocol/src/packets/play/message_acknowledgment_c2s.rs @@ -2,5 +2,5 @@ use crate::{Decode, Encode, Packet, VarInt}; #[derive(Copy, Clone, Debug, Encode, Decode, Packet)] pub struct MessageAcknowledgmentC2s { - pub message_count: VarInt, + pub message_index: VarInt, } diff --git a/crates/valence_protocol/src/packets/play/player_list_s2c.rs b/crates/valence_protocol/src/packets/play/player_list_s2c.rs index f07f930ee..85825903e 100644 --- a/crates/valence_protocol/src/packets/play/player_list_s2c.rs +++ b/crates/valence_protocol/src/packets/play/player_list_s2c.rs @@ -5,6 +5,7 @@ use bitfield_struct::bitfield; use uuid::Uuid; use valence_text::Text; +use crate::packets::play::player_session_c2s::PlayerSessionData; use crate::profile::Property; use crate::{Decode, Encode, GameMode, Packet, VarInt}; @@ -118,18 +119,9 @@ pub struct PlayerListEntry<'a> { pub player_uuid: Uuid, pub username: &'a str, pub properties: Cow<'a, [Property]>, - pub chat_data: Option>, + pub chat_data: Option>, pub listed: bool, pub ping: i32, pub game_mode: GameMode, pub display_name: Option>, } - -#[derive(Clone, PartialEq, Debug, Encode, Decode)] -pub struct ChatData<'a> { - pub session_id: Uuid, - /// Unix timestamp in milliseconds. - pub key_expiry_time: i64, - pub public_key: &'a [u8], - pub public_key_signature: &'a [u8], -} diff --git a/crates/valence_protocol/src/packets/play/player_session_c2s.rs b/crates/valence_protocol/src/packets/play/player_session_c2s.rs index 4b34135a0..3214c3219 100644 --- a/crates/valence_protocol/src/packets/play/player_session_c2s.rs +++ b/crates/valence_protocol/src/packets/play/player_session_c2s.rs @@ -1,12 +1,29 @@ +use std::borrow::Cow; + use uuid::Uuid; -use crate::{Bounded, Decode, Encode, Packet}; +use crate::{Decode, Encode, Packet}; + +#[derive(Clone, Debug, Encode, Decode, Packet)] +pub struct PlayerSessionC2s<'a>(pub Cow<'a, PlayerSessionData>); -#[derive(Copy, Clone, Debug, Encode, Decode, Packet)] -pub struct PlayerSessionC2s<'a> { +#[derive(Clone, PartialEq, Debug, Encode, Decode)] +pub struct PlayerSessionData { pub session_id: Uuid, // Public key pub expires_at: i64, - pub public_key_data: Bounded<&'a [u8], 512>, - pub key_signature: Bounded<&'a [u8], 4096>, + pub public_key_data: Box<[u8]>, + pub key_signature: Box<[u8]>, +} + +impl<'a> From for Cow<'a, PlayerSessionData> { + fn from(value: PlayerSessionData) -> Self { + Cow::Owned(value) + } +} + +impl<'a> From<&'a PlayerSessionData> for Cow<'a, PlayerSessionData> { + fn from(value: &'a PlayerSessionData) -> Self { + Cow::Borrowed(value) + } } diff --git a/crates/valence_registry/Cargo.toml b/crates/valence_registry/Cargo.toml index 8d75d1d25..c44dc264f 100644 --- a/crates/valence_registry/Cargo.toml +++ b/crates/valence_registry/Cargo.toml @@ -20,3 +20,4 @@ anyhow.workspace = true valence_ident.workspace = true valence_nbt = { workspace = true, features = ["serde"] } valence_server_common.workspace = true +valence_text.workspace = true diff --git a/crates/valence_registry/src/chat_type.rs b/crates/valence_registry/src/chat_type.rs new file mode 100644 index 000000000..27b57a211 --- /dev/null +++ b/crates/valence_registry/src/chat_type.rs @@ -0,0 +1,240 @@ +//! ChatType configuration and identification. +//! +//! **NOTE:** +//! - Modifying the chat type registry after the server has started can +//! break invariants within instances and clients! Make sure there are no +//! instances or clients spawned before mutating. + +use std::fmt; +use std::ops::{Deref, DerefMut}; + +use bevy_app::prelude::*; +use bevy_ecs::prelude::*; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; +use serde::de; +use tracing::error; +use valence_ident::{ident, Ident}; +use valence_nbt::serde::CompoundSerializer; +use valence_text::Color; + +use crate::codec::{RegistryCodec, RegistryValue}; +use crate::{Registry, RegistryIdx, RegistrySet}; + +pub struct ChatTypePlugin; + +impl Plugin for ChatTypePlugin { + fn build(&self, app: &mut bevy_app::App) { + app.init_resource::() + .add_systems(PreStartup, load_default_chat_types) + .add_systems(PostUpdate, update_chat_type_registry.before(RegistrySet)); + } +} + +fn load_default_chat_types(mut reg: ResMut, codec: Res) { + let mut helper = move || -> anyhow::Result<()> { + for value in codec.registry(ChatTypeRegistry::KEY) { + let chat_type = ChatType::deserialize(value.element.clone())?; + + reg.insert(value.name.clone(), chat_type); + } + + reg.swap_to_front(ident!("chat")); + + Ok(()) + }; + + if let Err(e) = helper() { + error!("failed to load default chat types from registry codec: {e:#}"); + } +} + +/// Add new chat types to or update existing chat types in the registry. +fn update_chat_type_registry(reg: Res, mut codec: ResMut) { + if reg.is_changed() { + let chat_types = codec.registry_mut(ChatTypeRegistry::KEY); + + chat_types.clear(); + + chat_types.extend(reg.iter().map(|(_, name, chat_type)| { + RegistryValue { + name: name.into(), + element: chat_type + .serialize(CompoundSerializer) + .expect("failed to serialize chat type"), + } + })); + } +} + +#[derive(Resource, Default, Debug)] +pub struct ChatTypeRegistry { + reg: Registry, +} + +impl ChatTypeRegistry { + pub const KEY: Ident<&str> = ident!("chat_type"); +} + +impl Deref for ChatTypeRegistry { + type Target = Registry; + + fn deref(&self) -> &Self::Target { + &self.reg + } +} + +impl DerefMut for ChatTypeRegistry { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.reg + } +} + +/// An index into the chat type registry +#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Debug)] +pub struct ChatTypeId(pub u16); + +impl ChatTypeId { + pub const DEFAULT: Self = ChatTypeId(0); +} + +impl RegistryIdx for ChatTypeId { + const MAX: usize = u32::MAX as _; + + #[inline] + fn to_index(self) -> usize { + self.0 as _ + } + + #[inline] + fn from_index(idx: usize) -> Self { + Self(idx as _) + } +} + +/// Contains information about how chat is styled, such as the chat color. The +/// notchian server has different chat types for team chat and direct messages. +/// +/// Note that [`ChatTypeDecoration::style`] for [`ChatType::narration`] +/// is unused by the notchian client and is ignored. +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct ChatType { + pub chat: ChatTypeDecoration, + pub narration: ChatTypeDecoration, +} + +#[derive(Serialize, Deserialize, Clone, Default, Debug)] +pub struct ChatTypeDecoration { + pub translation_key: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub style: Option, + pub parameters: ChatTypeParameters, +} + +#[derive(Serialize, Deserialize, Clone, Default, Debug)] +pub struct ChatTypeStyle { + #[serde(skip_serializing_if = "Option::is_none")] + pub color: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub bold: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub italic: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub underlined: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub strikethrough: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub obfuscated: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub insertion: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub font: Option>, + // TODO + // * click_event: Option, + // * hover_event: Option, +} + +#[derive(Clone, Copy, Default, Debug)] +pub struct ChatTypeParameters { + sender: bool, + target: bool, + content: bool, +} + +impl Default for ChatType { + fn default() -> Self { + Self { + chat: ChatTypeDecoration { + translation_key: "chat.type.text".into(), + style: None, + parameters: ChatTypeParameters { + sender: true, + content: true, + ..Default::default() + }, + }, + narration: ChatTypeDecoration { + translation_key: "chat.type.text.narrate".into(), + style: None, + parameters: ChatTypeParameters { + sender: true, + content: true, + ..Default::default() + }, + }, + } + } +} + +impl Serialize for ChatTypeParameters { + fn serialize(&self, serializer: S) -> Result + where S: Serializer + { + let mut args = vec![]; + if self.sender { + args.push("sender"); + } + if self.target { + args.push("target"); + } + if self.content { + args.push("content"); + } + args.serialize(serializer) + } +} + +impl<'de> Deserialize<'de> for ChatTypeParameters { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + struct ParameterVisitor; + + impl<'de> de::Visitor<'de> for ParameterVisitor { + type Value = ChatTypeParameters; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("struct ChatTypeParameters") + } + + fn visit_seq(self, mut seq: V) -> Result + where + V: de::SeqAccess<'de>, + { + let mut value = Self::Value::default(); + while let Some(element) = seq.next_element::()? { + match element.as_str() { + "sender" => value.sender = true, + "target" => value.target = true, + "content" => value.content = true, + _ => return Err(de::Error::unknown_field(&element, FIELDS)), + } + } + Ok(value) + } + } + + const FIELDS: &'static [&'static str] = &["sender", "target", "content"]; + deserializer.deserialize_struct("ChatTypeParameters", FIELDS, ParameterVisitor) + } +} diff --git a/crates/valence_registry/src/lib.rs b/crates/valence_registry/src/lib.rs index d9a59b105..7c3b66423 100644 --- a/crates/valence_registry/src/lib.rs +++ b/crates/valence_registry/src/lib.rs @@ -19,6 +19,7 @@ pub mod biome; pub mod codec; +pub mod chat_type; pub mod dimension_type; pub mod tags; diff --git a/crates/valence_server/src/lib.rs b/crates/valence_server/src/lib.rs index feb9a5902..54932d4a8 100644 --- a/crates/valence_server/src/lib.rs +++ b/crates/valence_server/src/lib.rs @@ -32,7 +32,6 @@ pub mod interact_entity; pub mod interact_item; pub mod keepalive; pub mod layer; -pub mod message; pub mod movement; pub mod op_level; pub mod resource_pack; diff --git a/crates/valence_server/src/message.rs b/crates/valence_server/src/message.rs deleted file mode 100644 index 54e4b5c03..000000000 --- a/crates/valence_server/src/message.rs +++ /dev/null @@ -1,63 +0,0 @@ -// TODO: delete this module in favor of valence_chat. - -use bevy_app::prelude::*; -use bevy_ecs::prelude::*; -use valence_protocol::encode::WritePacket; -use valence_protocol::packets::play::{ChatMessageC2s, GameMessageS2c}; -use valence_protocol::text::IntoText; - -use crate::event_loop::{EventLoopPreUpdate, PacketEvent}; - -pub struct MessagePlugin; - -impl Plugin for MessagePlugin { - fn build(&self, app: &mut App) { - app.add_event::() - .add_systems(EventLoopPreUpdate, handle_chat_message); - } -} - -pub trait SendMessage { - /// Sends a system message visible in the chat. - fn send_chat_message<'a>(&mut self, msg: impl IntoText<'a>); - /// Displays a message in the player's action bar (text above the hotbar). - fn send_action_bar_message<'a>(&mut self, msg: impl IntoText<'a>); -} - -impl SendMessage for T { - fn send_chat_message<'a>(&mut self, msg: impl IntoText<'a>) { - self.write_packet(&GameMessageS2c { - chat: msg.into_cow_text(), - overlay: false, - }); - } - - fn send_action_bar_message<'a>(&mut self, msg: impl IntoText<'a>) { - self.write_packet(&GameMessageS2c { - chat: msg.into_cow_text(), - overlay: true, - }); - } -} - -#[derive(Event, Clone, Debug)] -pub struct ChatMessageEvent { - pub client: Entity, - pub message: Box, - pub timestamp: u64, -} - -pub fn handle_chat_message( - mut packets: EventReader, - mut events: EventWriter, -) { - for packet in packets.iter() { - if let Some(pkt) = packet.decode::() { - events.send(ChatMessageEvent { - client: packet.client, - message: pkt.message.0.into(), - timestamp: pkt.timestamp, - }); - } - } -} diff --git a/examples/anvil_loading.rs b/examples/anvil_loading.rs index 664ba21dd..34a0c9cfa 100644 --- a/examples/anvil_loading.rs +++ b/examples/anvil_loading.rs @@ -4,7 +4,7 @@ use std::path::PathBuf; use clap::Parser; use valence::abilities::{FlyingSpeed, FovModifier, PlayerAbilitiesFlags}; -use valence::message::SendMessage; +use valence::chat::message::SendMessage; use valence::prelude::*; use valence_anvil::{AnvilLevel, ChunkLoadEvent, ChunkLoadStatus}; @@ -137,7 +137,7 @@ fn handle_chunk_loads( ); eprintln!("{errmsg}"); - layer.send_chat_message(errmsg.color(Color::RED)); + layer.send_game_message(errmsg.color(Color::RED)); layer.insert_chunk(event.pos, UnloadedChunk::new()); } diff --git a/examples/block_entities.rs b/examples/block_entities.rs index 937db43ca..d07c29121 100644 --- a/examples/block_entities.rs +++ b/examples/block_entities.rs @@ -1,7 +1,7 @@ #![allow(clippy::type_complexity)] use valence::interact_block::InteractBlockEvent; -use valence::message::ChatMessageEvent; +use valence::chat::message::ChatMessageEvent; use valence::nbt::{compound, List}; use valence::prelude::*; diff --git a/examples/boss_bar.rs b/examples/boss_bar.rs index a8aba6968..bf8013e19 100644 --- a/examples/boss_bar.rs +++ b/examples/boss_bar.rs @@ -7,7 +7,7 @@ use valence_boss_bar::{ BossBarTitle, }; use valence_server::entity::cow::CowEntityBundle; -use valence_server::message::ChatMessageEvent; +use valence::chat::message::{ChatMessageEvent, SendMessage}; use valence_text::color::NamedColor; const SPAWN_Y: i32 = 64; @@ -107,30 +107,30 @@ fn init_clients( pos.set([0.5, SPAWN_Y as f64 + 1.0, 0.5]); *game_mode = GameMode::Creative; - client.send_chat_message( + client.send_game_message( "Type 'view' to toggle bar display" .on_click_suggest_command("view") .on_hover_show_text("Type 'view'"), ); - client.send_chat_message( + client.send_game_message( "Type 'color' to set a random color" .on_click_suggest_command("color") .on_hover_show_text("Type 'color'"), ); - client.send_chat_message( + client.send_game_message( "Type 'division' to set a random division" .on_click_suggest_command("division") .on_hover_show_text("Type 'division'"), ); - client.send_chat_message( + client.send_game_message( "Type 'flags' to set random flags" .on_click_suggest_command("flags") .on_hover_show_text("Type 'flags'"), ); - client.send_chat_message( + client.send_game_message( "Type any string to set the title".on_click_suggest_command("title"), ); - client.send_chat_message( + client.send_game_message( "Type any number between 0 and 1 to set the health".on_click_suggest_command("health"), ); } diff --git a/examples/building.rs b/examples/building.rs index cbf6e774d..72e57d9ba 100644 --- a/examples/building.rs +++ b/examples/building.rs @@ -79,7 +79,7 @@ fn init_clients( pos.set([0.0, SPAWN_Y as f64 + 1.0, 0.0]); *game_mode = GameMode::Creative; - client.send_chat_message("Welcome to Valence! Build something cool.".italic()); + client.send_game_message("Welcome to Valence! Build something cool.".italic()); } } diff --git a/examples/chat.rs b/examples/chat.rs new file mode 100644 index 000000000..1ee370761 --- /dev/null +++ b/examples/chat.rs @@ -0,0 +1,115 @@ +#![allow(clippy::type_complexity)] + +use tracing::warn; +use valence::prelude::*; +use valence::chat::ChatState; +use valence::chat::message::{ChatMessageEvent, CommandExecutionEvent, SendMessage}; + +const SPAWN_Y: i32 = 64; + +pub fn main() { + App::new() + .add_plugins(DefaultPlugins) + .add_systems(Startup, setup) + .add_systems(Update, ( + init_clients, + despawn_disconnected_clients, + handle_command_events, + handle_message_events, + )) + .run(); +} + +fn setup( + mut commands: Commands, + server: Res, + dimensions: Res, + biomes: Res, +) { + let mut layer = LayerBundle::new(ident!("overworld"), &dimensions, &biomes, &server); + + for z in -5..5 { + for x in -5..5 { + layer.chunk.insert_chunk([x, z], UnloadedChunk::new()); + } + } + + for z in -25..25 { + for x in -25..25 { + layer + .chunk + .set_block([x, SPAWN_Y, z], BlockState::GRASS_BLOCK); + } + } + + commands.spawn(layer); +} + +fn init_clients( + mut clients: Query< + ( + &mut Client, + &mut Position, + &mut EntityLayerId, + &mut VisibleChunkLayer, + &mut VisibleEntityLayers, + &mut GameMode, + ), + Added, + >, + layers: Query, With)>, +) { + for ( + mut client, + mut pos, + mut layer_id, + mut visible_chunk_layer, + mut visible_entity_layers, + mut game_mode, + ) in &mut clients + { + let layer = layers.single(); + + pos.0 = [0.0, SPAWN_Y as f64 + 1.0, 0.0].into(); + layer_id.0 = layer; + visible_chunk_layer.0 = layer; + visible_entity_layers.0.insert(layer); + *game_mode = GameMode::Creative; + + client.send_game_message("Welcome to Valence! Say something.".italic()); + } +} + +fn handle_message_events( + mut clients: Query<(&mut Client, &mut ChatState)>, + names: Query<&Username>, + mut messages: EventReader +) { + for message in messages.iter() { + let sender_name = names.get(message.client).expect("Error getting username"); + // Need to find better way. Username is sender, while client and chat state are recievers. + // Maybe try to add a chat feature to Client. + for (mut client, mut state) in clients.iter_mut() { + state.as_mut().send_chat_message(client.as_mut(), sender_name, message).expect("Error sending message"); + } + } +} + +fn handle_command_events( + mut clients: Query<&mut Client>, + mut commands: EventReader, +) { + for command in commands.iter() { + let Ok(mut client) = clients.get_component_mut::(command.client) else { + warn!("Unable to find client for message: {:?}", command); + continue; + }; + + let message = command.command.to_string(); + + let formatted = + "You sent the command ".into_text() + ("/".into_text() + (message).into_text()).bold(); + + client.send_game_message(formatted); + } +} diff --git a/examples/cow_sphere.rs b/examples/cow_sphere.rs index e9f9ba124..455bde743 100644 --- a/examples/cow_sphere.rs +++ b/examples/cow_sphere.rs @@ -4,7 +4,7 @@ use std::f64::consts::TAU; use valence::abilities::{PlayerStartFlyingEvent, PlayerStopFlyingEvent}; use valence::math::{DQuat, EulerRot}; -use valence::message::SendMessage; +use valence::chat::message::SendMessage; use valence::prelude::*; use valence_text::color::NamedColor; diff --git a/examples/ctf.rs b/examples/ctf.rs index b6ad1491f..1f4f05ec7 100644 --- a/examples/ctf.rs +++ b/examples/ctf.rs @@ -396,7 +396,7 @@ fn init_clients( *game_mode = GameMode::Adventure; health.0 = PLAYER_MAX_HEALTH; - client.send_chat_message( + client.send_game_message( "Welcome to Valence! Select a team by jumping in the team's portal.".italic(), ); } @@ -460,7 +460,7 @@ fn digging( (Team::Blue, BlockState::RED_WOOL) => { if event.position == globals.red_flag { commands.entity(event.client).insert(HasFlag(Team::Red)); - client.send_chat_message("You have the flag!".italic()); + client.send_game_message("You have the flag!".italic()); flag_manager.red = Some(ent); return; } @@ -468,7 +468,7 @@ fn digging( (Team::Red, BlockState::BLUE_WOOL) => { if event.position == globals.blue_flag { commands.entity(event.client).insert(HasFlag(Team::Blue)); - client.send_chat_message("You have the flag!".italic()); + client.send_game_message("You have the flag!".italic()); flag_manager.blue = Some(ent); return; } @@ -621,7 +621,7 @@ fn do_team_selector_portals( look.pitch = 0.0; head_yaw.0 = yaw; let chat_text: Text = "You are on team ".into_text() + team.team_text() + "!"; - client.send_chat_message(chat_text); + client.send_game_message(chat_text); let main_layer = main_layers.single(); ent_layers.as_mut().0.remove(&main_layer); @@ -776,13 +776,13 @@ fn do_flag_capturing( }; if capture_trigger.contains_pos(position.0) { - client.send_chat_message("You captured the flag!".italic()); + client.send_game_message("You captured the flag!".italic()); score .scores .entry(*team) .and_modify(|score| *score += 1) .or_insert(1); - client.send_chat_message(score.render_scores()); + client.send_game_message(score.render_scores()); commands.entity(ent).remove::(); match has_flag.0 { Team::Red => flag_manager.red = None, diff --git a/examples/death.rs b/examples/death.rs index 645d76d45..019b7d11e 100644 --- a/examples/death.rs +++ b/examples/death.rs @@ -81,7 +81,7 @@ fn init_clients( pos.set([0.0, SPAWN_Y as f64 + 1.0, 0.0]); *game_mode = GameMode::Creative; - client.send_chat_message( + client.send_game_message( "Welcome to Valence! Sneak to die in the game (but not in real life).".italic(), ); } diff --git a/examples/entity_hitbox.rs b/examples/entity_hitbox.rs index 59eff1a31..3e2060a36 100644 --- a/examples/entity_hitbox.rs +++ b/examples/entity_hitbox.rs @@ -77,7 +77,7 @@ fn init_clients( pos.set([0.0, 65.0, 0.0]); *game_mode = GameMode::Creative; - client.send_chat_message("To spawn an entity, press shift. F3 + B to activate hitboxes"); + client.send_game_message("To spawn an entity, press shift. F3 + B to activate hitboxes"); } } diff --git a/examples/game_of_life.rs b/examples/game_of_life.rs index d7624fd43..a6c61d0c4 100644 --- a/examples/game_of_life.rs +++ b/examples/game_of_life.rs @@ -101,8 +101,8 @@ fn init_clients( pos.set([0.0, 65.0, 0.0]); *game_mode = GameMode::Survival; - client.send_chat_message("Welcome to Conway's game of life in Minecraft!".italic()); - client.send_chat_message( + client.send_game_message("Welcome to Conway's game of life in Minecraft!".italic()); + client.send_game_message( "Sneak to toggle running the simulation and the left mouse button to bring blocks to \ life." .italic(), diff --git a/examples/parkour.rs b/examples/parkour.rs index 806d1cdd7..ac9e003fd 100644 --- a/examples/parkour.rs +++ b/examples/parkour.rs @@ -70,7 +70,7 @@ fn init_clients( is_flat.0 = true; *game_mode = GameMode::Adventure; - client.send_chat_message("Welcome to epic infinite parkour game!".italic()); + client.send_game_message("Welcome to epic infinite parkour game!".italic()); let state = GameState { blocks: VecDeque::new(), @@ -100,7 +100,7 @@ fn reset_clients( if out_of_bounds || state.is_added() { if out_of_bounds && !state.is_added() { - client.send_chat_message( + client.send_game_message( "Your score was ".italic() + state .score diff --git a/examples/player_list.rs b/examples/player_list.rs index 8b3635286..c2ccf3c20 100644 --- a/examples/player_list.rs +++ b/examples/player_list.rs @@ -87,7 +87,7 @@ fn init_clients( pos.set([0.0, SPAWN_Y as f64 + 1.0, 0.0]); *game_mode = GameMode::Creative; - client.send_chat_message( + client.send_game_message( "Please open your player list (tab key)." .italic() .color(Color::WHITE), diff --git a/examples/resource_pack.rs b/examples/resource_pack.rs index b337aadad..59116e92a 100644 --- a/examples/resource_pack.rs +++ b/examples/resource_pack.rs @@ -1,7 +1,7 @@ #![allow(clippy::type_complexity)] use valence::entity::sheep::SheepEntityBundle; -use valence::message::SendMessage; +use valence::chat::message::SendMessage; use valence::prelude::*; use valence::protocol::packets::play::ResourcePackStatusC2s; use valence::resource_pack::ResourcePackStatusEvent; @@ -86,7 +86,7 @@ fn init_clients( pos.set([0.0, SPAWN_Y as f64 + 1.0, 0.0]); *game_mode = GameMode::Creative; - client.send_chat_message("Hit the sheep to prompt for the resource pack.".italic()); + client.send_game_message("Hit the sheep to prompt for the resource pack.".italic()); } } @@ -113,16 +113,16 @@ fn on_resource_pack_status( if let Ok(mut client) = clients.get_mut(event.client) { match event.status { ResourcePackStatusC2s::Accepted => { - client.send_chat_message("Resource pack accepted.".color(Color::GREEN)); + client.send_game_message("Resource pack accepted.".color(Color::GREEN)); } ResourcePackStatusC2s::Declined => { - client.send_chat_message("Resource pack declined.".color(Color::RED)); + client.send_game_message("Resource pack declined.".color(Color::RED)); } ResourcePackStatusC2s::FailedDownload => { - client.send_chat_message("Resource pack failed to download.".color(Color::RED)); + client.send_game_message("Resource pack failed to download.".color(Color::RED)); } ResourcePackStatusC2s::SuccessfullyLoaded => { - client.send_chat_message( + client.send_game_message( "Resource pack successfully downloaded.".color(Color::BLUE), ); } diff --git a/examples/text.rs b/examples/text.rs index 8a7ed9eb7..60d3c7001 100644 --- a/examples/text.rs +++ b/examples/text.rs @@ -69,17 +69,17 @@ fn init_clients( visible_entity_layers.0.insert(layer); *game_mode = GameMode::Creative; - client.send_chat_message("Welcome to the text example.".bold()); - client.send_chat_message( + client.send_game_message("Welcome to the text example.".bold()); + client.send_game_message( "The following examples show ways to use the different text components.", ); // Text examples - client.send_chat_message("\nText"); - client.send_chat_message(" - ".into_text() + Text::text("Plain text")); - client.send_chat_message(" - ".into_text() + Text::text("Styled text").italic()); - client.send_chat_message(" - ".into_text() + Text::text("Colored text").color(Color::GOLD)); - client.send_chat_message( + client.send_game_message("\nText"); + client.send_game_message(" - ".into_text() + Text::text("Plain text")); + client.send_game_message(" - ".into_text() + Text::text("Styled text").italic()); + client.send_game_message(" - ".into_text() + Text::text("Colored text").color(Color::GOLD)); + client.send_game_message( " - ".into_text() + Text::text("Colored and styled text") .color(Color::GOLD) @@ -88,61 +88,61 @@ fn init_clients( ); // Translated text examples - client.send_chat_message("\nTranslated Text"); - client.send_chat_message( + client.send_game_message("\nTranslated Text"); + client.send_game_message( " - 'chat.type.advancement.task': ".into_text() + Text::translate(keys::CHAT_TYPE_ADVANCEMENT_TASK, []), ); - client.send_chat_message( + client.send_game_message( " - 'chat.type.advancement.task' with slots: ".into_text() + Text::translate( keys::CHAT_TYPE_ADVANCEMENT_TASK, ["arg1".into(), "arg2".into()], ), ); - client.send_chat_message( + client.send_game_message( " - 'custom.translation_key': ".into_text() + Text::translate("custom.translation_key", []), ); // Scoreboard value example - client.send_chat_message("\nScoreboard Values"); - client.send_chat_message(" - Score: ".into_text() + Text::score("*", "objective", None)); - client.send_chat_message( + client.send_game_message("\nScoreboard Values"); + client.send_game_message(" - Score: ".into_text() + Text::score("*", "objective", None)); + client.send_game_message( " - Score with custom value: ".into_text() + Text::score("*", "objective", Some("value".into())), ); // Entity names example - client.send_chat_message("\nEntity Names (Selector)"); - client.send_chat_message(" - Nearest player: ".into_text() + Text::selector("@p", None)); - client.send_chat_message(" - Random player: ".into_text() + Text::selector("@r", None)); - client.send_chat_message(" - All players: ".into_text() + Text::selector("@a", None)); - client.send_chat_message(" - All entities: ".into_text() + Text::selector("@e", None)); - client.send_chat_message( + client.send_game_message("\nEntity Names (Selector)"); + client.send_game_message(" - Nearest player: ".into_text() + Text::selector("@p", None)); + client.send_game_message(" - Random player: ".into_text() + Text::selector("@r", None)); + client.send_game_message(" - All players: ".into_text() + Text::selector("@a", None)); + client.send_game_message(" - All entities: ".into_text() + Text::selector("@e", None)); + client.send_game_message( " - All entities with custom separator: ".into_text() + Text::selector("@e", Some(", ".into_text().color(Color::GOLD))), ); // Keybind example - client.send_chat_message("\nKeybind"); + client.send_game_message("\nKeybind"); client - .send_chat_message(" - 'key.inventory': ".into_text() + Text::keybind("key.inventory")); + .send_game_message(" - 'key.inventory': ".into_text() + Text::keybind("key.inventory")); // NBT examples - client.send_chat_message("\nNBT"); - client.send_chat_message( + client.send_game_message("\nNBT"); + client.send_game_message( " - Block NBT: ".into_text() + Text::block_nbt("{}", "0 1 0", None, None), ); - client.send_chat_message( + client.send_game_message( " - Entity NBT: ".into_text() + Text::entity_nbt("{}", "@a", None, None), ); - client.send_chat_message( + client.send_game_message( " - Storage NBT: ".into_text() + Text::storage_nbt(ident!("storage.key"), "@a", None, None), ); - client.send_chat_message( + client.send_game_message( "\n\n↑ ".into_text().bold().color(Color::GOLD) + "Scroll up to see the full example!".into_text().not_bold(), ); diff --git a/examples/world_border.rs b/examples/world_border.rs index bb7111c30..b7b818523 100644 --- a/examples/world_border.rs +++ b/examples/world_border.rs @@ -3,7 +3,7 @@ use bevy_app::App; use valence::client::despawn_disconnected_clients; use valence::inventory::HeldItem; -use valence::message::{ChatMessageEvent, SendMessage}; +use valence::chat::message::{ChatMessageEvent, SendMessage}; use valence::prelude::*; use valence::world_border::*; @@ -93,14 +93,14 @@ fn init_clients( let pickaxe = ItemStack::new(ItemKind::WoodenPickaxe, 1, None); inv.set_slot(main_slot.slot(), pickaxe); client - .send_chat_message("Use `add` and `center` chat messages to change the world border."); + .send_game_message("Use `add` and `center` chat messages to change the world border."); } } fn display_diameter(mut layers: Query<(&mut ChunkLayer, &WorldBorderLerp)>) { for (mut layer, lerp) in &mut layers { if lerp.remaining_ticks > 0 { - layer.send_chat_message(format!("diameter = {}", lerp.current_diameter)); + layer.send_game_message(format!("diameter = {}", lerp.current_diameter)); } } } diff --git a/src/lib.rs b/src/lib.rs index dfffdb3db..7bd54ec83 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -43,6 +43,8 @@ pub use valence_advancement as advancement; pub use valence_anvil as anvil; #[cfg(feature = "boss_bar")] pub use valence_boss_bar as boss_bar; +#[cfg(feature = "chat")] +pub use valence_chat as chat; #[cfg(feature = "inventory")] pub use valence_inventory as inventory; pub use valence_lang as lang; @@ -68,7 +70,6 @@ use valence_server::interact_entity::InteractEntityPlugin; use valence_server::interact_item::InteractItemPlugin; use valence_server::keepalive::KeepalivePlugin; use valence_server::layer::LayerPlugin; -use valence_server::message::MessagePlugin; use valence_server::movement::MovementPlugin; use valence_server::op_level::OpLevelPlugin; use valence_server::resource_pack::ResourcePackPlugin; @@ -101,6 +102,8 @@ pub mod prelude { event::AdvancementTabChangeEvent, Advancement, AdvancementBundle, AdvancementClientUpdate, AdvancementCriteria, AdvancementDisplay, AdvancementFrameType, AdvancementRequirements, }; + #[cfg(feature = "chat")] + pub use valence_chat::message::SendMessage as _; #[cfg(feature = "inventory")] pub use valence_inventory::{ CursorItem, Inventory, InventoryKind, InventoryWindow, InventoryWindowMut, OpenInventory, @@ -139,7 +142,6 @@ pub mod prelude { }; pub use valence_server::layer::{EntityLayer, LayerBundle}; pub use valence_server::math::{DVec2, DVec3, Vec2, Vec3}; - pub use valence_server::message::SendMessage as _; pub use valence_server::nbt::Compound; pub use valence_server::protocol::packets::play::particle_s2c::Particle; pub use valence_server::protocol::text::{Color, IntoText, Text}; @@ -181,7 +183,6 @@ impl PluginGroup for DefaultPlugins { .add(ClientSettingsPlugin) .add(ActionPlugin) .add(TeleportPlugin) - .add(MessagePlugin) .add(CustomPayloadPlugin) .add(HandSwingPlugin) .add(InteractBlockPlugin) @@ -241,6 +242,11 @@ impl PluginGroup for DefaultPlugins { group = group.add(valence_scoreboard::ScoreboardPlugin); } + #[cfg(feature = "chat")] + { + group = group.add(valence_chat::ChatPlugin); + } + group } } From 803e59467c82ecddd739419d8c786f4956be91b6 Mon Sep 17 00:00:00 2001 From: Guac Date: Fri, 29 Sep 2023 17:23:14 -0600 Subject: [PATCH 22/32] Run fmt --- crates/valence_chat/src/lib.rs | 52 ++++++++++++++---------- crates/valence_chat/src/message.rs | 7 ++-- crates/valence_player_list/src/lib.rs | 13 +++++- crates/valence_registry/src/chat_type.rs | 10 ++--- crates/valence_registry/src/lib.rs | 2 +- examples/block_entities.rs | 2 +- examples/boss_bar.rs | 2 +- examples/chat.rs | 30 ++++++++------ examples/cow_sphere.rs | 2 +- examples/resource_pack.rs | 2 +- examples/world_border.rs | 2 +- 11 files changed, 73 insertions(+), 51 deletions(-) diff --git a/crates/valence_chat/src/lib.rs b/crates/valence_chat/src/lib.rs index 5cc0d863f..abe592454 100644 --- a/crates/valence_chat/src/lib.rs +++ b/crates/valence_chat/src/lib.rs @@ -26,18 +26,19 @@ use std::time::SystemTime; use bevy_app::prelude::*; use bevy_ecs::prelude::*; use tracing::warn; - +use valence_lang::keys::{CHAT_DISABLED_OPTIONS, DISCONNECT_GENERIC_REASON}; +use valence_protocol::packets::play::client_settings_c2s::ChatMode; +use valence_protocol::packets::play::{ChatMessageC2s, CommandExecutionC2s}; use valence_registry::chat_type::ChatTypePlugin; -use valence_server::event_loop::{EventLoopPreUpdate, PacketEvent}; use valence_server::client::{Client, SpawnClientsSet}; use valence_server::client_settings::ClientSettings; -use valence_protocol::packets::play::client_settings_c2s::ChatMode; -use valence_protocol::packets::play::{ChatMessageC2s, CommandExecutionC2s}; -use valence_server::protocol::WritePacket; -use valence_server::protocol::packets::play::chat_message_s2c::{MessageFilterType, MessageSignature}; +use valence_server::event_loop::{EventLoopPreUpdate, PacketEvent}; +use valence_server::protocol::packets::play::chat_message_s2c::{ + MessageFilterType, MessageSignature, +}; use valence_server::protocol::packets::play::{ChatMessageS2c, ProfilelessChatMessageS2c}; +use valence_server::protocol::WritePacket; use valence_text::{Color, Text}; -use valence_lang::keys::{CHAT_DISABLED_OPTIONS, DISCONNECT_GENERIC_REASON}; #[cfg(feature = "secure")] use { anyhow::bail, @@ -48,9 +49,6 @@ use { sha1::{Digest, Sha1}, sha2::Sha256, uuid::Uuid, - valence_server::client::{DisconnectClient, Username}, - valence_server::protocol::packets::play::{MessageAcknowledgmentC2s, PlayerSessionC2s}, - valence_text::IntoText, valence_lang::keys::{ CHAT_DISABLED_CHAIN_BROKEN, CHAT_DISABLED_EXPIRED_PROFILE_KEY, CHAT_DISABLED_MISSING_PROFILE_KEY, MULTIPLAYER_DISCONNECT_CHAT_VALIDATION_FAILED, @@ -59,8 +57,11 @@ use { MULTIPLAYER_DISCONNECT_OUT_OF_ORDER_CHAT, MULTIPLAYER_DISCONNECT_TOO_MANY_PENDING_CHATS, MULTIPLAYER_DISCONNECT_UNSIGNED_CHAT, }, - valence_server_common::UniqueId, valence_player_list::{ChatSession, PlayerListEntry}, + valence_server::client::{DisconnectClient, Username}, + valence_server::protocol::packets::play::{MessageAcknowledgmentC2s, PlayerSessionC2s}, + valence_server_common::UniqueId, + valence_text::IntoText, }; use crate::message::{ChatMessageEvent, ChatMessageType, CommandExecutionEvent, SendMessage}; @@ -73,10 +74,7 @@ pub struct ChatPlugin; impl Plugin for ChatPlugin { fn build(&self, app: &mut bevy_app::App) { app.add_plugins(ChatTypePlugin) - .add_systems( - PreUpdate, - init_chat_states.after(SpawnClientsSet), - ) + .add_systems(PreUpdate, init_chat_states.after(SpawnClientsSet)) .add_systems( EventLoopPreUpdate, ( @@ -143,9 +141,20 @@ impl Default for ChatState { #[cfg(feature = "secure")] impl ChatState { - pub fn send_chat_message(&mut self, client: &mut Client, username: &Username, message: &ChatMessageEvent) -> anyhow::Result<()> { + pub fn send_chat_message( + &mut self, + client: &mut Client, + username: &Username, + message: &ChatMessageEvent, + ) -> anyhow::Result<()> { match &message.message_type { - ChatMessageType::Signed { salt, signature, message_index, last_seen, sender } => { + ChatMessageType::Signed { + salt, + signature, + message_index, + last_seen, + sender, + } => { // Create a list of messages that have been seen by the client. let previous = last_seen .iter() @@ -629,10 +638,9 @@ fn handle_message_packets( }; let Some(link) = &state.chain.next_link() else { - client.send_game_message(Text::translate( - CHAT_DISABLED_CHAIN_BROKEN, - [], - ).color(Color::RED)); + client.send_game_message( + Text::translate(CHAT_DISABLED_CHAIN_BROKEN, []).color(Color::RED), + ); continue; }; @@ -640,7 +648,7 @@ fn handle_message_packets( warn!("Player `{}` doesn't have a chat session", username.0); commands.add(DisconnectClient { client: packet.client, - reason: Text::translate(CHAT_DISABLED_MISSING_PROFILE_KEY, []) + reason: Text::translate(CHAT_DISABLED_MISSING_PROFILE_KEY, []), }); continue; }; diff --git a/crates/valence_chat/src/message.rs b/crates/valence_chat/src/message.rs index 1d68a7247..f38c8f052 100644 --- a/crates/valence_chat/src/message.rs +++ b/crates/valence_chat/src/message.rs @@ -1,12 +1,11 @@ use bevy_app::prelude::*; use bevy_ecs::prelude::*; +#[cfg(feature = "secure")] +use uuid::Uuid; use valence_protocol::encode::WritePacket; use valence_protocol::packets::play::GameMessageS2c; use valence_protocol::text::IntoText; -#[cfg(feature = "secure")] -use uuid::Uuid; - pub(super) fn build(app: &mut App) { app.add_event::() .add_event::(); @@ -62,4 +61,4 @@ pub enum ChatMessageType { last_seen: Vec<[u8; 256]>, }, Unsigned, -} \ No newline at end of file +} diff --git a/crates/valence_player_list/src/lib.rs b/crates/valence_player_list/src/lib.rs index 0d0ed31da..f30abc179 100644 --- a/crates/valence_player_list/src/lib.rs +++ b/crates/valence_player_list/src/lib.rs @@ -28,10 +28,10 @@ use valence_server::client::{Client, Properties, Username}; use valence_server::keepalive::Ping; use valence_server::layer::UpdateLayersPreClientSet; use valence_server::protocol::encode::PacketWriter; +use valence_server::protocol::packets::play::player_session_c2s::PlayerSessionData; use valence_server::protocol::packets::play::{ player_list_s2c as packet, PlayerListHeaderS2c, PlayerListS2c, PlayerRemoveS2c, }; -use valence_server::protocol::packets::play::player_session_c2s::PlayerSessionData; use valence_server::protocol::WritePacket; use valence_server::text::IntoText; use valence_server::uuid::Uuid; @@ -228,7 +228,16 @@ fn init_player_list_for_clients( let entries: Vec<_> = entries .iter() .map( - |(uuid, username, props, game_mode, ping, display_name, listed, chat_session, )| { + |( + uuid, + username, + props, + game_mode, + ping, + display_name, + listed, + chat_session, + )| { packet::PlayerListEntry { player_uuid: uuid.0, username: &username.0, diff --git a/crates/valence_registry/src/chat_type.rs b/crates/valence_registry/src/chat_type.rs index 27b57a211..c6bcc9cc3 100644 --- a/crates/valence_registry/src/chat_type.rs +++ b/crates/valence_registry/src/chat_type.rs @@ -10,8 +10,7 @@ use std::ops::{Deref, DerefMut}; use bevy_app::prelude::*; use bevy_ecs::prelude::*; -use serde::{Deserialize, Deserializer, Serialize, Serializer}; -use serde::de; +use serde::{de, Deserialize, Deserializer, Serialize, Serializer}; use tracing::error; use valence_ident::{ident, Ident}; use valence_nbt::serde::CompoundSerializer; @@ -39,7 +38,7 @@ fn load_default_chat_types(mut reg: ResMut, codec: Res(&self, serializer: S) -> Result - where S: Serializer + where + S: Serializer, { let mut args = vec![]; if self.sender { @@ -204,7 +204,7 @@ impl Serialize for ChatTypeParameters { } impl<'de> Deserialize<'de> for ChatTypeParameters { - fn deserialize(deserializer: D) -> Result + fn deserialize(deserializer: D) -> Result where D: Deserializer<'de>, { diff --git a/crates/valence_registry/src/lib.rs b/crates/valence_registry/src/lib.rs index 7c3b66423..304fbd86a 100644 --- a/crates/valence_registry/src/lib.rs +++ b/crates/valence_registry/src/lib.rs @@ -18,8 +18,8 @@ )] pub mod biome; -pub mod codec; pub mod chat_type; +pub mod codec; pub mod dimension_type; pub mod tags; diff --git a/examples/block_entities.rs b/examples/block_entities.rs index d07c29121..b8f8c8ca7 100644 --- a/examples/block_entities.rs +++ b/examples/block_entities.rs @@ -1,7 +1,7 @@ #![allow(clippy::type_complexity)] -use valence::interact_block::InteractBlockEvent; use valence::chat::message::ChatMessageEvent; +use valence::interact_block::InteractBlockEvent; use valence::nbt::{compound, List}; use valence::prelude::*; diff --git a/examples/boss_bar.rs b/examples/boss_bar.rs index bf8013e19..bb62cb3ca 100644 --- a/examples/boss_bar.rs +++ b/examples/boss_bar.rs @@ -1,13 +1,13 @@ #![allow(clippy::type_complexity)] use rand::seq::SliceRandom; +use valence::chat::message::{ChatMessageEvent, SendMessage}; use valence::prelude::*; use valence_boss_bar::{ BossBarBundle, BossBarColor, BossBarDivision, BossBarFlags, BossBarHealth, BossBarStyle, BossBarTitle, }; use valence_server::entity::cow::CowEntityBundle; -use valence::chat::message::{ChatMessageEvent, SendMessage}; use valence_text::color::NamedColor; const SPAWN_Y: i32 = 64; diff --git a/examples/chat.rs b/examples/chat.rs index 1ee370761..f918f6aff 100644 --- a/examples/chat.rs +++ b/examples/chat.rs @@ -1,9 +1,9 @@ #![allow(clippy::type_complexity)] use tracing::warn; -use valence::prelude::*; -use valence::chat::ChatState; use valence::chat::message::{ChatMessageEvent, CommandExecutionEvent, SendMessage}; +use valence::chat::ChatState; +use valence::prelude::*; const SPAWN_Y: i32 = 64; @@ -11,12 +11,15 @@ pub fn main() { App::new() .add_plugins(DefaultPlugins) .add_systems(Startup, setup) - .add_systems(Update, ( - init_clients, - despawn_disconnected_clients, - handle_command_events, - handle_message_events, - )) + .add_systems( + Update, + ( + init_clients, + despawn_disconnected_clients, + handle_command_events, + handle_message_events, + ), + ) .run(); } @@ -83,14 +86,17 @@ fn init_clients( fn handle_message_events( mut clients: Query<(&mut Client, &mut ChatState)>, names: Query<&Username>, - mut messages: EventReader + mut messages: EventReader, ) { for message in messages.iter() { let sender_name = names.get(message.client).expect("Error getting username"); - // Need to find better way. Username is sender, while client and chat state are recievers. - // Maybe try to add a chat feature to Client. + // Need to find better way. Username is sender, while client and chat state are + // recievers. Maybe try to add a chat feature to Client. for (mut client, mut state) in clients.iter_mut() { - state.as_mut().send_chat_message(client.as_mut(), sender_name, message).expect("Error sending message"); + state + .as_mut() + .send_chat_message(client.as_mut(), sender_name, message) + .expect("Error sending message"); } } } diff --git a/examples/cow_sphere.rs b/examples/cow_sphere.rs index 455bde743..d8b9e5056 100644 --- a/examples/cow_sphere.rs +++ b/examples/cow_sphere.rs @@ -3,8 +3,8 @@ use std::f64::consts::TAU; use valence::abilities::{PlayerStartFlyingEvent, PlayerStopFlyingEvent}; -use valence::math::{DQuat, EulerRot}; use valence::chat::message::SendMessage; +use valence::math::{DQuat, EulerRot}; use valence::prelude::*; use valence_text::color::NamedColor; diff --git a/examples/resource_pack.rs b/examples/resource_pack.rs index 59116e92a..959c3650c 100644 --- a/examples/resource_pack.rs +++ b/examples/resource_pack.rs @@ -1,7 +1,7 @@ #![allow(clippy::type_complexity)] -use valence::entity::sheep::SheepEntityBundle; use valence::chat::message::SendMessage; +use valence::entity::sheep::SheepEntityBundle; use valence::prelude::*; use valence::protocol::packets::play::ResourcePackStatusC2s; use valence::resource_pack::ResourcePackStatusEvent; diff --git a/examples/world_border.rs b/examples/world_border.rs index b7b818523..1af1887f9 100644 --- a/examples/world_border.rs +++ b/examples/world_border.rs @@ -1,9 +1,9 @@ #![allow(clippy::type_complexity)] use bevy_app::App; +use valence::chat::message::{ChatMessageEvent, SendMessage}; use valence::client::despawn_disconnected_clients; use valence::inventory::HeldItem; -use valence::chat::message::{ChatMessageEvent, SendMessage}; use valence::prelude::*; use valence::world_border::*; From 126dedfbbe1db486f3cd272663ac53bd31b7c5dd Mon Sep 17 00:00:00 2001 From: Guac Date: Sun, 1 Oct 2023 00:33:27 -0600 Subject: [PATCH 23/32] Cleanup checks --- assets/depgraph.svg | 267 ++++++++++++----------- crates/valence_chat/src/lib.rs | 4 +- crates/valence_registry/src/chat_type.rs | 2 +- examples/chat.rs | 2 +- 4 files changed, 138 insertions(+), 137 deletions(-) diff --git a/assets/depgraph.svg b/assets/depgraph.svg index 892e1d726..d01547c2e 100644 --- a/assets/depgraph.svg +++ b/assets/depgraph.svg @@ -1,396 +1,397 @@ - - - + + - +%3 + 0 - -valence_advancement + +valence_advancement 1 - -valence_server + +valence_server 0->1 - - + + 2 - -valence_entity + +valence_entity 1->2 - - + + 11 - -valence_registry + +valence_registry 1->11 - - + + 10 - -valence_server_common + +valence_server_common 2->10 - - + + 11->10 - - + + 6 - -valence_protocol + +valence_protocol 10->6 - - + + 3 - -valence_math + +valence_math 4 - -valence_nbt + +valence_nbt 5 - -valence_ident + +valence_ident 7 - -valence_generated + +valence_generated 6->7 - - + + 9 - -valence_text + +valence_text 6->9 - - + + 7->3 - - + + 7->5 - - + + 9->4 - - + + 9->5 - - + + 8 - -valence_build_utils + +valence_build_utils 12 - -valence_anvil + +valence_anvil 12->1 - - + + 13 - -valence_boss_bar + +valence_boss_bar 13->1 - - + + 14 - -valence_chat + +valence_chat 15 - -valence_lang + +valence_lang 14->15 - - + + 16 - -valence_player_list + +valence_player_list 14->16 - - + + 16->1 - - + + 17 - -valence_inventory + +valence_inventory 17->1 - - + + 18 - -valence_network + +valence_network 18->1 - - + + 18->15 - - + + 19 - -valence_scoreboard + +valence_scoreboard 19->1 - - + + 20 - -valence_spatial + +valence_spatial 21 - -valence_weather + +valence_weather 21->1 - - + + 22 - -valence_world_border + +valence_world_border 22->1 - - + + 23 - -dump_schedule + +dump_schedule 24 - -valence + +valence 23->24 - - + + 24->0 - - + + 24->12 - - + + 24->13 - - + + 24->14 - - + + 24->17 - - + + 24->18 - - + + 24->19 - - + + 24->21 - - + + 24->22 - - + + 25 - -packet_inspector + +packet_inspector 25->6 - - + + 26 - -playground + +playground 26->24 - - + + 27 - -stresser + +stresser 27->6 - - + + diff --git a/crates/valence_chat/src/lib.rs b/crates/valence_chat/src/lib.rs index abe592454..edcccb373 100644 --- a/crates/valence_chat/src/lib.rs +++ b/crates/valence_chat/src/lib.rs @@ -197,7 +197,7 @@ impl ChatState { /// Updates the chat state's previously seen signatures with a new one /// `signature`. - fn add_pending(&mut self, last_seen: &Vec<[u8; 256]>, signature: &[u8; 256]) { + fn add_pending(&mut self, last_seen: &[[u8; 256]], signature: &[u8; 256]) { self.signature_storage.add(last_seen, signature); self.validator.add_pending(signature); } @@ -392,7 +392,7 @@ impl MessageSignatureStorage { /// `signature` to the storage. /// /// Warning: this consumes `last_seen`. - fn add(&mut self, last_seen: &Vec<[u8; 256]>, signature: &[u8; 256]) { + fn add(&mut self, last_seen: &[[u8; 256]], signature: &[u8; 256]) { let mut sig_set = FxHashSet::default(); last_seen diff --git a/crates/valence_registry/src/chat_type.rs b/crates/valence_registry/src/chat_type.rs index c6bcc9cc3..857cc4ddb 100644 --- a/crates/valence_registry/src/chat_type.rs +++ b/crates/valence_registry/src/chat_type.rs @@ -234,7 +234,7 @@ impl<'de> Deserialize<'de> for ChatTypeParameters { } } - const FIELDS: &'static [&'static str] = &["sender", "target", "content"]; + const FIELDS: &[&str] = &["sender", "target", "content"]; deserializer.deserialize_struct("ChatTypeParameters", FIELDS, ParameterVisitor) } } diff --git a/examples/chat.rs b/examples/chat.rs index f918f6aff..800e70350 100644 --- a/examples/chat.rs +++ b/examples/chat.rs @@ -91,7 +91,7 @@ fn handle_message_events( for message in messages.iter() { let sender_name = names.get(message.client).expect("Error getting username"); // Need to find better way. Username is sender, while client and chat state are - // recievers. Maybe try to add a chat feature to Client. + // receivers. Maybe try to add a chat feature to Client. for (mut client, mut state) in clients.iter_mut() { state .as_mut() From 7beed3a7568360eddfaaca09ec8170f4b349f09d Mon Sep 17 00:00:00 2001 From: Guac Date: Sun, 1 Oct 2023 01:00:41 -0600 Subject: [PATCH 24/32] More cleanup --- crates/valence_chat/src/chat_type.rs | 328 ----------------------- crates/valence_registry/src/chat_type.rs | 2 +- 2 files changed, 1 insertion(+), 329 deletions(-) delete mode 100644 crates/valence_chat/src/chat_type.rs diff --git a/crates/valence_chat/src/chat_type.rs b/crates/valence_chat/src/chat_type.rs deleted file mode 100644 index c03cb094e..000000000 --- a/crates/valence_chat/src/chat_type.rs +++ /dev/null @@ -1,328 +0,0 @@ -//! ChatType configuration and identification. -//! -//! **NOTE:** -//! -//! - Modifying the chat type registry after the server has started can -//! break invariants within instances and clients! Make sure there are no -//! instances or clients spawned before mutating. - -use std::ops::Index; - -use anyhow::{bail, Context}; -use bevy_app::{CoreSet, Plugin, StartupSet}; -use bevy_ecs::prelude::*; -use tracing::error; -use valence_core::ident; -use valence_core::ident::Ident; -use valence_core::text::Color; -use valence_nbt::{compound, Compound, List, Value}; -use valence_registry::{RegistryCodec, RegistryCodecSet, RegistryValue}; - -pub(crate) struct ChatTypePlugin; - -impl Plugin for ChatTypePlugin { - fn build(&self, app: &mut bevy_app::App) { - app.insert_resource(ChatTypeRegistry { - id_to_chat_type: vec![], - }) - .add_systems( - (update_chat_type_registry, remove_chat_types_from_registry) - .chain() - .in_base_set(CoreSet::PostUpdate) - .before(RegistryCodecSet), - ) - .add_startup_system(load_default_chat_types.in_base_set(StartupSet::PreStartup)); - } -} - -#[derive(Resource)] -pub struct ChatTypeRegistry { - id_to_chat_type: Vec, -} - -impl ChatTypeRegistry { - pub const KEY: Ident<&str> = ident!("minecraft:chat_type"); - - pub fn get_by_id(&self, id: ChatTypeId) -> Option { - self.id_to_chat_type.get(id.0 as usize).cloned() - } - - pub fn iter(&self) -> impl Iterator + '_ { - self.id_to_chat_type - .iter() - .enumerate() - .map(|(id, chat_type)| (ChatTypeId(id as _), *chat_type)) - } -} - -impl Index for ChatTypeRegistry { - type Output = Entity; - - fn index(&self, index: ChatTypeId) -> &Self::Output { - self.id_to_chat_type - .get(index.0 as usize) - .unwrap_or_else(|| panic!("invalid {index:?}")) - } -} - -/// An index into the chat type registry -#[derive(Component, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Default)] -pub struct ChatTypeId(pub u16); - -/// Contains information about how chat is styled, such as the chat color. The -/// notchian server has different chat types for team chat and direct messages. -/// -/// Note that [`ChatTypeDecoration::style`] for [`ChatType::narration`] -/// is unused by the notchian client and is ignored. -#[derive(Component, Clone, Debug)] -pub struct ChatType { - pub name: Ident, - pub chat: ChatTypeDecoration, - pub narration: ChatTypeDecoration, -} - -impl Default for ChatType { - fn default() -> Self { - Self { - name: ident!("chat").into(), - chat: ChatTypeDecoration { - translation_key: "chat.type.text".into(), - style: None, - parameters: ChatTypeParameters { - sender: true, - content: true, - ..Default::default() - }, - }, - narration: ChatTypeDecoration { - translation_key: "chat.type.text.narrate".into(), - style: None, - parameters: ChatTypeParameters { - sender: true, - content: true, - ..Default::default() - }, - }, - } - } -} - -#[derive(Clone, PartialEq, Default, Debug)] -pub struct ChatTypeDecoration { - pub translation_key: String, - pub style: Option, - pub parameters: ChatTypeParameters, -} - -#[derive(Clone, PartialEq, Default, Debug)] -pub struct ChatTypeStyle { - pub color: Option, - pub bold: Option, - pub italic: Option, - pub underlined: Option, - pub strikethrough: Option, - pub obfuscated: Option, - pub insertion: Option, - pub font: Option>, - // TODO - // * click_event: Option, - // * hover_event: Option, -} - -#[derive(Clone, Copy, PartialEq, Eq, Default, Debug)] -pub struct ChatTypeParameters { - sender: bool, - target: bool, - content: bool, -} - -fn load_default_chat_types( - mut reg: ResMut, - codec: Res, - mut commands: Commands, -) { - let mut helper = move || { - for value in codec.registry(ChatTypeRegistry::KEY) { - let Some(Value::Compound(chat)) = value.element.get("chat") else { - bail!("missing chat type text decorations") - }; - - let chat_key = chat - .get("translation_key") - .and_then(|v| v.as_string()) - .context("invalid translation_key")? - .clone(); - - let chat_parameters = - if let Some(Value::List(List::String(params))) = chat.get("parameters") { - ChatTypeParameters { - sender: params.contains(&String::from("sender")), - target: params.contains(&String::from("target")), - content: params.contains(&String::from("content")), - } - } else { - bail!("missing chat type text parameters") - }; - - let Some(Value::Compound(narration)) = value.element.get("narration") else { - bail!("missing chat type text narration decorations") - }; - - let narration_key = narration - .get("translation_key") - .and_then(|v| v.as_string()) - .context("invalid translation_key")? - .clone(); - - let narration_parameters = - if let Some(Value::List(List::String(params))) = chat.get("parameters") { - ChatTypeParameters { - sender: params.contains(&String::from("sender")), - target: params.contains(&String::from("target")), - content: params.contains(&String::from("content")), - } - } else { - bail!("missing chat type narration parameters") - }; - - let entity = commands - .spawn(ChatType { - name: value.name.clone(), - chat: ChatTypeDecoration { - translation_key: chat_key, - // TODO: Add support for the chat type styling - style: None, - parameters: chat_parameters, - }, - narration: ChatTypeDecoration { - translation_key: narration_key, - style: None, - parameters: narration_parameters, - }, - }) - .id(); - - reg.id_to_chat_type.push(entity); - } - - Ok(()) - }; - - if let Err(e) = helper() { - error!("failed to load default chat types from registry codec: {e:#}"); - } -} - -/// Add new chat types to or update existing chat types in the registry. -fn update_chat_type_registry( - mut reg: ResMut, - mut codec: ResMut, - chat_types: Query<(Entity, &ChatType), Changed>, -) { - for (entity, chat_type) in &chat_types { - let chat_type_registry = codec.registry_mut(ChatTypeRegistry::KEY); - - let mut chat_text_compound = compound! { - "translation_key" => &chat_type.chat.translation_key, - "parameters" => { - let mut parameters = Vec::new(); - if chat_type.chat.parameters.sender { - parameters.push("sender".to_string()); - } - if chat_type.chat.parameters.target { - parameters.push("target".to_string()); - } - if chat_type.chat.parameters.content { - parameters.push("content".to_string()); - } - List::String(parameters) - }, - }; - - if let Some(style) = &chat_type.chat.style { - let mut s = Compound::new(); - if let Some(color) = style.color { - s.insert( - "color", - format!("#{:02x}{:02x}{:02x}", color.r, color.g, color.b), - ); - } - if let Some(bold) = style.bold { - s.insert("bold", bold); - } - if let Some(italic) = style.italic { - s.insert("italic", italic); - } - if let Some(underlined) = style.underlined { - s.insert("underlined", underlined); - } - if let Some(strikethrough) = style.strikethrough { - s.insert("strikethrough", strikethrough); - } - if let Some(obfuscated) = style.obfuscated { - s.insert("obfuscated", obfuscated); - } - if let Some(insertion) = &style.insertion { - s.insert("insertion", insertion.clone()); - } - if let Some(font) = &style.font { - s.insert("font", font.clone()); - } - chat_text_compound.insert("style", s); - } - - let chat_narration_compound = compound! { - "translation_key" => &chat_type.narration.translation_key, - "parameters" => { - let mut parameters = Vec::new(); - if chat_type.narration.parameters.sender { - parameters.push("sender".to_string()); - } - if chat_type.narration.parameters.target { - parameters.push("target".to_string()); - } - if chat_type.narration.parameters.content { - parameters.push("content".to_string()); - } - List::String(parameters) - }, - }; - - let chat_type_compound = compound! { - "chat" => chat_text_compound, - "narration" => chat_narration_compound, - }; - - if let Some(value) = chat_type_registry - .iter_mut() - .find(|v| v.name == chat_type.name) - { - value.name = chat_type.name.clone(); - value.element.merge(chat_type_compound); - } else { - chat_type_registry.push(RegistryValue { - name: chat_type.name.clone(), - element: chat_type_compound, - }); - reg.id_to_chat_type.push(entity); - } - } -} - -/// Remove deleted chat types from the registry. -fn remove_chat_types_from_registry( - mut chat_types: RemovedComponents, - mut reg: ResMut, - mut codec: ResMut, -) { - for chat_type in chat_types.iter() { - if let Some(idx) = reg - .id_to_chat_type - .iter() - .position(|entity| *entity == chat_type) - { - reg.id_to_chat_type.remove(idx); - codec.registry_mut(ChatTypeRegistry::KEY).remove(idx); - } - } -} diff --git a/crates/valence_registry/src/chat_type.rs b/crates/valence_registry/src/chat_type.rs index 857cc4ddb..4e1a745cd 100644 --- a/crates/valence_registry/src/chat_type.rs +++ b/crates/valence_registry/src/chat_type.rs @@ -71,7 +71,7 @@ pub struct ChatTypeRegistry { } impl ChatTypeRegistry { - pub const KEY: Ident<&str> = ident!("chat_type"); + pub const KEY: Ident<&'static str> = ident!("chat_type"); } impl Deref for ChatTypeRegistry { From bbdfa6f8ff58fb6ba718957e2ca564d3879d78b7 Mon Sep 17 00:00:00 2001 From: Guac Date: Mon, 2 Oct 2023 21:07:23 -0600 Subject: [PATCH 25/32] Add commands to message chain --- crates/valence_chat/src/command.rs | 26 ++++++ crates/valence_chat/src/lib.rs | 84 +++++++++++++++++-- crates/valence_chat/src/message.rs | 10 +-- .../src/packets/play/command_execution_c2s.rs | 4 +- examples/chat.rs | 3 +- 5 files changed, 109 insertions(+), 18 deletions(-) create mode 100644 crates/valence_chat/src/command.rs diff --git a/crates/valence_chat/src/command.rs b/crates/valence_chat/src/command.rs new file mode 100644 index 000000000..60625f52a --- /dev/null +++ b/crates/valence_chat/src/command.rs @@ -0,0 +1,26 @@ +// TODO: Eventually this should be moved to a `valence_commands` crate + +use bevy_app::prelude::*; +use bevy_ecs::prelude::*; + +pub(super) fn build(app: &mut App) { + app.add_event::(); +} + +#[derive(Event, Clone, Debug)] +pub struct CommandExecutionEvent { + pub client: Entity, + pub command: Box, + pub timestamp: u64, + #[cfg(feature = "secure")] + pub salt: u64, + #[cfg(feature = "secure")] + pub argument_signatures: Vec, +} + +#[cfg(feature = "secure")] +#[derive(Clone, Debug)] +pub struct ArgumentSignature { + pub name: String, + pub signature: Box<[u8; 256]>, +} diff --git a/crates/valence_chat/src/lib.rs b/crates/valence_chat/src/lib.rs index edcccb373..2290809ef 100644 --- a/crates/valence_chat/src/lib.rs +++ b/crates/valence_chat/src/lib.rs @@ -17,6 +17,7 @@ clippy::dbg_macro )] +pub mod command; pub mod message; #[cfg(feature = "secure")] @@ -41,6 +42,8 @@ use valence_server::protocol::WritePacket; use valence_text::{Color, Text}; #[cfg(feature = "secure")] use { + crate::command::ArgumentSignature, + crate::message::ChatMessageType, anyhow::bail, rsa::pkcs1v15::Pkcs1v15Sign, rsa::pkcs8::DecodePublicKey, @@ -64,7 +67,8 @@ use { valence_text::IntoText, }; -use crate::message::{ChatMessageEvent, ChatMessageType, CommandExecutionEvent, SendMessage}; +use crate::command::CommandExecutionEvent; +use crate::message::{ChatMessageEvent, SendMessage}; #[cfg(feature = "secure")] const MOJANG_KEY_DATA: &[u8] = include_bytes!("../yggdrasil_session_pubkey.der"); @@ -98,6 +102,7 @@ impl Plugin for ChatPlugin { ); } + command::build(app); message::build(app); } } @@ -751,16 +756,83 @@ fn handle_message_packets( } fn handle_command_packets( + mut clients: Query< + (&mut ChatState, &mut Client, &Username, &ClientSettings), + With, + >, + _sessions: Query<&ChatSession, With>, mut packets: EventReader, mut command_events: EventWriter, + mut commands: Commands, ) { for packet in packets.iter() { - if let Some(command) = packet.decode::() { - command_events.send(CommandExecutionEvent { + let Some(command) = packet.decode::() else { + continue; + }; + + let Ok((mut state, mut client, username, settings)) = clients.get_mut(packet.client) else { + warn!("Unable to find client for message '{:?}'", command); + continue; + }; + + // Ensure that the client isn't sending messages while their chat is hidden. + if settings.chat_mode == ChatMode::Hidden { + client.send_game_message(Text::translate(CHAT_DISABLED_OPTIONS, []).color(Color::RED)); + continue; + } + + // Ensure we are receiving chat messages in order. + if command.timestamp < state.last_message_timestamp { + warn!( + "{:?} sent out-of-order chat: '{:?}'", + username.0, command.command + ); + commands.add(DisconnectClient { client: packet.client, - command: command.command.0.into(), - timestamp: command.timestamp, - }) + reason: Text::translate(MULTIPLAYER_DISCONNECT_OUT_OF_ORDER_CHAT, []), + }); + continue; } + + state.last_message_timestamp = command.timestamp; + + // Validate the message acknowledgements. + let _last_seen = match state + .validator + .validate(&command.acknowledgement.0, command.message_index.0) + { + Err(error) => { + warn!( + "Failed to validate acknowledgements from `{}`: {}", + username.0, error + ); + commands.add(DisconnectClient { + client: packet.client, + reason: Text::translate(MULTIPLAYER_DISCONNECT_CHAT_VALIDATION_FAILED, []), + }); + continue; + } + Ok(last_seen) => last_seen, + }; + + // TODO: Implement proper argument verification + // This process will invlove both `_sessions` and `_last_seen` + + warn!("{:?}", command); + command_events.send(CommandExecutionEvent { + client: packet.client, + command: command.command.0.into(), + timestamp: command.timestamp, + salt: command.salt, + argument_signatures: command + .argument_signatures + .0 + .iter() + .map(|sig| ArgumentSignature { + name: sig.argument_name.0.into(), + signature: (*sig.signature).into(), + }) + .collect(), + }) } } diff --git a/crates/valence_chat/src/message.rs b/crates/valence_chat/src/message.rs index f38c8f052..5004b5294 100644 --- a/crates/valence_chat/src/message.rs +++ b/crates/valence_chat/src/message.rs @@ -7,8 +7,7 @@ use valence_protocol::packets::play::GameMessageS2c; use valence_protocol::text::IntoText; pub(super) fn build(app: &mut App) { - app.add_event::() - .add_event::(); + app.add_event::(); } pub trait SendMessage { @@ -34,13 +33,6 @@ impl SendMessage for T { } } -#[derive(Event, Clone, Debug)] -pub struct CommandExecutionEvent { - pub client: Entity, - pub command: Box, - pub timestamp: u64, -} - #[derive(Event, Clone, Debug)] pub struct ChatMessageEvent { pub client: Entity, diff --git a/crates/valence_protocol/src/packets/play/command_execution_c2s.rs b/crates/valence_protocol/src/packets/play/command_execution_c2s.rs index b09d4fe0f..332211215 100644 --- a/crates/valence_protocol/src/packets/play/command_execution_c2s.rs +++ b/crates/valence_protocol/src/packets/play/command_execution_c2s.rs @@ -5,8 +5,8 @@ pub struct CommandExecutionC2s<'a> { pub command: Bounded<&'a str, 256>, pub timestamp: u64, pub salt: u64, - pub argument_signatures: Vec>, - pub message_count: VarInt, + pub argument_signatures: Bounded>, 8>, + pub message_index: VarInt, //// This is a bitset of 20; each bit represents one //// of the last 20 messages received and whether or not //// the message was acknowledged by the client diff --git a/examples/chat.rs b/examples/chat.rs index 800e70350..fafa38ed4 100644 --- a/examples/chat.rs +++ b/examples/chat.rs @@ -1,7 +1,8 @@ #![allow(clippy::type_complexity)] use tracing::warn; -use valence::chat::message::{ChatMessageEvent, CommandExecutionEvent, SendMessage}; +use valence::chat::command::CommandExecutionEvent; +use valence::chat::message::{ChatMessageEvent, SendMessage}; use valence::chat::ChatState; use valence::prelude::*; From 832d6544d0d8bbc086b1d27de5aea726128281a4 Mon Sep 17 00:00:00 2001 From: Guac Date: Tue, 3 Oct 2023 00:18:54 -0600 Subject: [PATCH 26/32] Fix typo --- crates/valence_chat/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/valence_chat/src/lib.rs b/crates/valence_chat/src/lib.rs index 2290809ef..3327a9405 100644 --- a/crates/valence_chat/src/lib.rs +++ b/crates/valence_chat/src/lib.rs @@ -816,7 +816,7 @@ fn handle_command_packets( }; // TODO: Implement proper argument verification - // This process will invlove both `_sessions` and `_last_seen` + // This process will involve both `_sessions` and `_last_seen` warn!("{:?}", command); command_events.send(CommandExecutionEvent { From db540c763677794588cc78003292b3bd8ed88d3b Mon Sep 17 00:00:00 2001 From: Guac Date: Mon, 23 Oct 2023 16:35:08 -0600 Subject: [PATCH 27/32] Update depgraph.svg --- assets/depgraph.svg | 513 ++++++++++++++++++++++---------------------- 1 file changed, 259 insertions(+), 254 deletions(-) diff --git a/assets/depgraph.svg b/assets/depgraph.svg index d01547c2e..a348506c3 100644 --- a/assets/depgraph.svg +++ b/assets/depgraph.svg @@ -1,397 +1,402 @@ - - - + + -%3 - + 0 - -valence_advancement + +java_string 1 - -valence_server - - - -0->1 - - + +valence_advancement 2 - -valence_entity + +valence_server - + 1->2 - - + + - + -11 - -valence_registry +3 + +valence_entity - - -1->11 - - + + +2->3 + + - + -10 - -valence_server_common +12 + +valence_registry - + + +2->12 + + + + + +11 + +valence_server_common + + -2->10 - - +3->11 + + - + -11->10 - - +12->11 + + - - -6 - -valence_protocol + + +7 + +valence_protocol - + -10->6 - - - - - -3 - -valence_math +11->7 + + 4 - -valence_nbt + +valence_math 5 - -valence_ident + +valence_nbt - - -7 - -valence_generated + + +6 + +valence_ident + + + +8 + +valence_generated - + -6->7 - - +7->8 + + - - -9 - -valence_text + + +10 + +valence_text - + -6->9 - - +7->10 + + - + -7->3 - - +8->4 + + - + -7->5 - - +8->6 + + - + -9->4 - - +10->5 + + - + -9->5 - - - - - -8 - -valence_build_utils +10->6 + + - + -12 - -valence_anvil - - - -12->1 - - +9 + +valence_build_utils 13 - -valence_boss_bar + +valence_anvil - - -13->1 - - + + +13->2 + + 14 - -valence_chat + +valence_boss_bar + + + +14->2 + + 15 - -valence_lang - - - -14->15 - - + +valence_chat 16 - -valence_player_list + +valence_lang - - -14->16 - - - - - -16->1 - - + + +15->16 + + 17 - -valence_inventory + +valence_player_list - - -17->1 - - + + +15->17 + + + + + +17->2 + + 18 - -valence_network - - - -18->1 - - + +valence_inventory - - -18->15 - - + + +18->2 + + 19 - -valence_scoreboard + +valence_network - - -19->1 - - + + +19->2 + + + + + +19->16 + + 20 - -valence_spatial + +valence_scoreboard + + + +20->2 + + 21 - -valence_weather - - - -21->1 - - + +valence_spatial 22 - -valence_world_border + +valence_weather - - -22->1 - - + + +22->2 + + 23 - -dump_schedule + +valence_world_border + + + +23->2 + + 24 - -valence + +dump_schedule + + + +25 + +valence - + -23->24 - - +24->25 + + - + -24->0 - - +25->1 + + - + -24->12 - - +25->13 + + - + -24->13 - - +25->14 + + - + -24->14 - - +25->15 + + - + -24->17 - - +25->18 + + - + -24->18 - - +25->19 + + - + -24->19 - - +25->20 + + - + -24->21 - - +25->22 + + - + -24->22 - - - - - -25 - -packet_inspector - - - -25->6 - - +25->23 + + 26 - -playground + +packet_inspector - - -26->24 - - + + +26->7 + + 27 - -stresser + +playground + + + +27->25 + + + + + +28 + +stresser - + -27->6 - - +28->7 + + From 38fa403c26fa3668bd40fff5ff7cbb34e9d8f9b7 Mon Sep 17 00:00:00 2001 From: Guac Date: Mon, 23 Oct 2023 17:07:27 -0600 Subject: [PATCH 28/32] Fix depgraph.svg --- assets/depgraph.svg | 271 ++++++++++++++++++++++---------------------- 1 file changed, 136 insertions(+), 135 deletions(-) diff --git a/assets/depgraph.svg b/assets/depgraph.svg index a348506c3..a6e87f805 100644 --- a/assets/depgraph.svg +++ b/assets/depgraph.svg @@ -1,402 +1,403 @@ - - - + + - +%3 + 0 - -java_string + +java_string 1 - -valence_advancement + +valence_advancement 2 - -valence_server + +valence_server 1->2 - - + + 3 - -valence_entity + +valence_entity 2->3 - - + + 12 - -valence_registry + +valence_registry 2->12 - - + + 11 - -valence_server_common + +valence_server_common 3->11 - - + + 12->11 - - + + 7 - -valence_protocol + +valence_protocol 11->7 - - + + 4 - -valence_math + +valence_math 5 - -valence_nbt + +valence_nbt 6 - -valence_ident + +valence_ident 8 - -valence_generated + +valence_generated 7->8 - - + + 10 - -valence_text + +valence_text 7->10 - - + + 8->4 - - + + 8->6 - - + + 10->5 - - + + 10->6 - - + + 9 - -valence_build_utils + +valence_build_utils 13 - -valence_anvil + +valence_anvil 13->2 - - + + 14 - -valence_boss_bar + +valence_boss_bar 14->2 - - + + 15 - -valence_chat + +valence_chat 16 - -valence_lang + +valence_lang 15->16 - - + + 17 - -valence_player_list + +valence_player_list 15->17 - - + + 17->2 - - + + 18 - -valence_inventory + +valence_inventory 18->2 - - + + 19 - -valence_network + +valence_network 19->2 - - + + 19->16 - - + + 20 - -valence_scoreboard + +valence_scoreboard 20->2 - - + + 21 - -valence_spatial + +valence_spatial 22 - -valence_weather + +valence_weather 22->2 - - + + 23 - -valence_world_border + +valence_world_border 23->2 - - + + 24 - -dump_schedule + +dump_schedule 25 - -valence + +valence 24->25 - - + + 25->1 - - + + 25->13 - - + + 25->14 - - + + 25->15 - - + + 25->18 - - + + 25->19 - - + + 25->20 - - + + 25->22 - - + + 25->23 - - + + 26 - -packet_inspector + +packet_inspector 26->7 - - + + 27 - -playground + +playground 27->25 - - + + 28 - -stresser + +stresser 28->7 - - + + From f0145f0cc5548dc1de51c5361c99b5fe370dcc25 Mon Sep 17 00:00:00 2001 From: Guac Date: Wed, 15 Nov 2023 15:14:03 -0700 Subject: [PATCH 29/32] Update depgraph.svg --- assets/depgraph.svg | 375 +++++++++++++++++++++++--------------------- 1 file changed, 196 insertions(+), 179 deletions(-) diff --git a/assets/depgraph.svg b/assets/depgraph.svg index a6e87f805..4cb26c577 100644 --- a/assets/depgraph.svg +++ b/assets/depgraph.svg @@ -1,403 +1,420 @@ - - - + + -%3 - + 0 - -java_string + +java_string 1 - -valence_advancement + +valence_advancement 2 - -valence_server + +valence_server 1->2 - - + + 3 - -valence_entity + +valence_entity 2->3 - - + + 12 - -valence_registry + +valence_registry 2->12 - - + + 11 - -valence_server_common + +valence_server_common 3->11 - - + + 12->11 - - + + 7 - -valence_protocol + +valence_protocol 11->7 - - + + 4 - -valence_math + +valence_math 5 - -valence_nbt + +valence_nbt 6 - -valence_ident + +valence_ident 8 - -valence_generated + +valence_generated 7->8 - - + + 10 - -valence_text + +valence_text 7->10 - - + + 8->4 - - + + 8->6 - - + + 10->5 - - + + 10->6 - - + + 9 - -valence_build_utils + +valence_build_utils 13 - -valence_anvil + +valence_anvil 13->2 - - + + 14 - -valence_boss_bar + +valence_boss_bar 14->2 - - + + 15 - -valence_chat + +valence_chat 16 - -valence_lang + +valence_lang 15->16 - - + + 17 - -valence_player_list + +valence_player_list 15->17 - - + + 17->2 - - + + 18 - -valence_inventory + +valence_command 18->2 - - + + 19 - -valence_network + +valence_inventory 19->2 - - - - - -19->16 - - + + 20 - -valence_scoreboard + +valence_network - + 20->2 - - + + + + + +20->16 + + 21 - -valence_spatial + +valence_scoreboard + + + +21->2 + + 22 - -valence_weather - - - -22->2 - - + +valence_spatial 23 - -valence_world_border + +valence_weather 23->2 - - + + 24 - -dump_schedule + +valence_world_border + + + +24->2 + + 25 - -valence + +dump_schedule - - -24->25 - - + + +26 + +valence - + -25->1 - - +25->26 + + - + -25->13 - - +26->1 + + - + -25->14 - - +26->13 + + - + -25->15 - - +26->14 + + - + -25->18 - - +26->15 + + - + -25->19 - - +26->18 + + - + -25->20 - - +26->19 + + - + -25->22 - - +26->20 + + - + -25->23 - - - - - -26 - -packet_inspector +26->21 + + - + -26->7 - - +26->23 + + + + + +26->24 + + 27 - -playground + +packet_inspector - - -27->25 - - + + +27->7 + + 28 - -stresser - - - -28->7 - - + +playground + + + +28->26 + + + + + +29 + +stresser + + + +29->7 + + From d42af9f226bb4adaf200814352f7bac254d3411f Mon Sep 17 00:00:00 2001 From: Guac Date: Wed, 15 Nov 2023 18:11:21 -0700 Subject: [PATCH 30/32] Fix parse error checking --- crates/valence_command/src/manager.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/valence_command/src/manager.rs b/crates/valence_command/src/manager.rs index b352abbbe..53cba0d0a 100644 --- a/crates/valence_command/src/manager.rs +++ b/crates/valence_command/src/manager.rs @@ -350,8 +350,9 @@ fn parse_command_args( let pre_input = input.clone().into_inner(); let valid = parser(&mut input); if valid { + // If input.len() > pre_input.len() the parser replaced the input let Some(arg) = pre_input - .get(..input.len() - pre_input.len()) + .get(..pre_input.len().wrapping_sub(input.len())) .map(|s| s.to_string()) else { panic!( From 83da57c2bdc8006c46a2934a76879748f258e1e9 Mon Sep 17 00:00:00 2001 From: Guac Date: Mon, 20 Nov 2023 14:49:16 -0700 Subject: [PATCH 31/32] Update command.rs --- examples/command.rs | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/examples/command.rs b/examples/command.rs index 703ec4fc5..e3e6fc999 100644 --- a/examples/command.rs +++ b/examples/command.rs @@ -331,7 +331,7 @@ fn find_targets( match target { None => { let client = &mut clients.get_mut(event.executor).unwrap().1; - client.send_chat_message(format!("Could not find target: {}", name)); + client.send_game_message(format!("Could not find target: {}", name)); vec![] } Some(target_entity) => { @@ -376,7 +376,7 @@ fn find_targets( match target { None => { let mut client = clients.get_mut(event.executor).unwrap().1; - client.send_chat_message("Could not find target".to_string()); + client.send_game_message("Could not find target".to_string()); vec![] } Some(target_entity) => { @@ -396,7 +396,7 @@ fn find_targets( match target { None => { let mut client = clients.get_mut(event.executor).unwrap().1; - client.send_chat_message("Could not find target".to_string()); + client.send_game_message("Could not find target".to_string()); vec![] } Some(target_entity) => { @@ -407,7 +407,7 @@ fn find_targets( }, EntitySelector::ComplexSelector(_, _) => { let mut client = clients.get_mut(event.executor).unwrap().1; - client.send_chat_message("complex selector not implemented".to_string()); + client.send_game_message("complex selector not implemented".to_string()); vec![] } } @@ -419,7 +419,7 @@ fn handle_test_command( ) { for event in events.iter() { let client = &mut clients.get_mut(event.executor).unwrap(); - client.send_chat_message(format!( + client.send_game_message(format!( "Test command executed with data:\n {:#?}", &event.result )); @@ -432,7 +432,7 @@ fn handle_complex_command( ) { for event in events.iter() { let client = &mut clients.get_mut(event.executor).unwrap(); - client.send_chat_message(format!( + client.send_game_message(format!( "complex command executed with data:\n {:#?}\n and with the modifiers:\n {:#?}", &event.result, &event.modifiers )); @@ -445,7 +445,7 @@ fn handle_struct_command( ) { for event in events.iter() { let client = &mut clients.get_mut(event.executor).unwrap(); - client.send_chat_message(format!( + client.send_game_message(format!( "Struct command executed with data:\n {:#?}", &event.result )); @@ -476,7 +476,7 @@ fn handle_gamemode_command( None => { let (mut client, mut game_mode, ..) = clients.get_mut(event.executor).unwrap(); *game_mode = game_mode_to_set; - client.send_chat_message(format!( + client.send_game_message(format!( "Gamemode command executor -> self executed with data:\n {:#?}", &event.result )); @@ -486,7 +486,7 @@ fn handle_gamemode_command( EntitySelectors::AllEntities => { for (mut client, mut game_mode, ..) in &mut clients.iter_mut() { *game_mode = game_mode_to_set; - client.send_chat_message(format!( + client.send_game_message(format!( "Gamemode command executor -> all entities executed with data:\n \ {:#?}", &event.result @@ -503,14 +503,14 @@ fn handle_gamemode_command( None => { let client = &mut clients.get_mut(event.executor).unwrap().0; client - .send_chat_message(format!("Could not find target: {}", name)); + .send_game_message(format!("Could not find target: {}", name)); } Some(target) => { let mut game_mode = clients.get_mut(target).unwrap().1; *game_mode = game_mode_to_set; let client = &mut clients.get_mut(event.executor).unwrap().0; - client.send_chat_message(format!( + client.send_game_message(format!( "Gamemode command executor -> single player executed with \ data:\n {:#?}", &event.result @@ -521,7 +521,7 @@ fn handle_gamemode_command( EntitySelectors::AllPlayers => { for (mut client, mut game_mode, ..) in &mut clients.iter_mut() { *game_mode = game_mode_to_set; - client.send_chat_message(format!( + client.send_game_message(format!( "Gamemode command executor -> all entities executed with data:\n \ {:#?}", &event.result @@ -532,7 +532,7 @@ fn handle_gamemode_command( let (mut client, mut game_mode, ..) = clients.get_mut(event.executor).unwrap(); *game_mode = game_mode_to_set; - client.send_chat_message(format!( + client.send_game_message(format!( "Gamemode command executor -> self executed with data:\n {:#?}", &event.result )); @@ -554,14 +554,14 @@ fn handle_gamemode_command( match target { None => { let client = &mut clients.get_mut(event.executor).unwrap().0; - client.send_chat_message("Could not find target".to_string()); + client.send_game_message("Could not find target".to_string()); } Some(target) => { let mut game_mode = clients.get_mut(target).unwrap().1; *game_mode = game_mode_to_set; let client = &mut clients.get_mut(event.executor).unwrap().0; - client.send_chat_message(format!( + client.send_game_message(format!( "Gamemode command executor -> single player executed with \ data:\n {:#?}", &event.result @@ -578,14 +578,14 @@ fn handle_gamemode_command( match target { None => { let client = &mut clients.get_mut(event.executor).unwrap().0; - client.send_chat_message("Could not find target".to_string()); + client.send_game_message("Could not find target".to_string()); } Some(target) => { let mut game_mode = clients.get_mut(target).unwrap().1; *game_mode = game_mode_to_set; let client = &mut clients.get_mut(event.executor).unwrap().0; - client.send_chat_message(format!( + client.send_game_message(format!( "Gamemode command executor -> single player executed with \ data:\n {:#?}", &event.result @@ -597,7 +597,7 @@ fn handle_gamemode_command( EntitySelector::ComplexSelector(_, _) => { let client = &mut clients.get_mut(event.executor).unwrap().0; client - .send_chat_message("Complex selectors are not implemented yet".to_string()); + .send_game_message("Complex selectors are not implemented yet".to_string()); } }, } From 7ac1f2b45c22a6b94da5bd8acbce55b88910caae Mon Sep 17 00:00:00 2001 From: Guac Date: Mon, 20 Nov 2023 14:49:36 -0700 Subject: [PATCH 32/32] Add message parser --- crates/valence_command/src/parsers.rs | 2 + crates/valence_command/src/parsers/message.rs | 105 ++++++++++++++++++ 2 files changed, 107 insertions(+) create mode 100644 crates/valence_command/src/parsers/message.rs diff --git a/crates/valence_command/src/parsers.rs b/crates/valence_command/src/parsers.rs index 956b70ee0..2407f296f 100644 --- a/crates/valence_command/src/parsers.rs +++ b/crates/valence_command/src/parsers.rs @@ -8,6 +8,7 @@ pub mod entity_anchor; pub mod entity_selector; pub mod gamemode; pub mod inventory_slot; +pub mod message; pub mod numbers; pub mod rotation; pub mod score_holder; @@ -24,6 +25,7 @@ pub use column_pos::ColumnPos; pub use entity_anchor::EntityAnchor; pub use entity_selector::EntitySelector; pub use inventory_slot::InventorySlot; +pub use message::{Message, MessageSelector}; pub use rotation::Rotation; pub use score_holder::ScoreHolder; pub use strings::{GreedyString, QuotableString}; diff --git a/crates/valence_command/src/parsers/message.rs b/crates/valence_command/src/parsers/message.rs new file mode 100644 index 000000000..426473fce --- /dev/null +++ b/crates/valence_command/src/parsers/message.rs @@ -0,0 +1,105 @@ +use super::Parser; +use crate::parsers::{CommandArg, CommandArgParseError, EntitySelector, ParseInput}; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Message { + pub message: String, + pub selectors: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct MessageSelector { + pub start: u32, + pub end: u32, + pub selector: EntitySelector, +} + +impl CommandArg for Message { + fn parse_arg(input: &mut ParseInput) -> Result { + input.skip_whitespace(); + + let message = input.clone().into_inner().to_string(); + let mut selectors: Vec = Vec::new(); + + let mut i = 0u32; + while let Some(c) = input.peek() { + if c == '@' { + let start = i; + let length_before = input.len(); + let selector = EntitySelector::parse_arg(input)?; + i += length_before as u32 - input.len() as u32; + selectors.push(MessageSelector { + start, + end: i, + selector, + }); + } else { + i += 1; + input.advance(); + } + } + + Ok(Message { message, selectors }) + } + + fn display() -> Parser { + Parser::Message + } +} + +#[test] +fn test_message() { + use crate::parsers::entity_selector::EntitySelectors; + + let mut input = ParseInput::new("Hello @e"); + assert_eq!( + Message::parse_arg(&mut input).unwrap(), + Message { + message: "Hello @e".to_string(), + selectors: vec![MessageSelector { + start: 6, + end: 8, + selector: EntitySelector::SimpleSelector(EntitySelectors::AllEntities) + }] + } + ); + assert!(input.is_done()); + + let mut input = ParseInput::new("@p say hi to @a"); + assert_eq!( + Message::parse_arg(&mut input).unwrap(), + Message { + message: "@p say hi to @a".to_string(), + selectors: vec![ + MessageSelector { + start: 0, + end: 2, + selector: EntitySelector::SimpleSelector(EntitySelectors::NearestPlayer) + }, + MessageSelector { + start: 13, + end: 15, + selector: EntitySelector::SimpleSelector(EntitySelectors::AllPlayers) + }, + ] + } + ); + assert!(input.is_done()); + + let mut input = ParseInput::new("say hi to nearby players @p[distance=..5]"); + assert_eq!( + Message::parse_arg(&mut input).unwrap(), + Message { + message: "say hi to nearby players @p[distance=..5]".to_string(), + selectors: vec![MessageSelector { + start: 25, + end: 41, + selector: EntitySelector::ComplexSelector( + EntitySelectors::NearestPlayer, + "distance=..5".to_string() + ) + },] + } + ); + assert!(input.is_done()); +}