diff --git a/examples/cow_sphere.rs b/examples/cow_sphere.rs index 094b6d5..5ab1ea6 100644 --- a/examples/cow_sphere.rs +++ b/examples/cow_sphere.rs @@ -97,6 +97,15 @@ impl Config for Game { if client.created_tick() == server.current_tick() { client.set_game_mode(GameMode::Creative); client.teleport([0.0, 200.0, 0.0], 0.0, 0.0); + + world.meta.player_list_mut().insert( + client.uuid(), + client.username().to_string(), + client.textures().cloned(), + client.game_mode(), + 0, + None, + ); } if client.is_disconnected() { diff --git a/examples/terrain.rs b/examples/terrain.rs index 4ba7711..f62bbde 100644 --- a/examples/terrain.rs +++ b/examples/terrain.rs @@ -54,7 +54,7 @@ impl Config for Game { fn online_mode(&self) -> bool { // You'll want this to be true on real servers. - true + false } async fn server_list_ping(&self, _server: &Server, _remote_addr: SocketAddr) -> ServerListPing { @@ -105,12 +105,11 @@ impl Config for Game { client.set_max_view_distance(32); client.teleport([0.0, 200.0, 0.0], 0.0, 0.0); - world.meta.player_list_mut().add_player( + world.meta.player_list_mut().insert( client.uuid(), client.username().to_string(), - None, - None, - GameMode::Creative, + client.textures().cloned(), + client.game_mode(), 0, None, ); diff --git a/src/client.rs b/src/client.rs index fd86e3c..1fcf512 100644 --- a/src/client.rs +++ b/src/client.rs @@ -25,14 +25,15 @@ use crate::packets::play::s2c::{ PlayerPositionAndLook, PlayerPositionAndLookFlags, RegistryCodec, S2cPlayPacket, SpawnPosition, UnloadChunk, UpdateViewDistance, UpdateViewPosition, }; +use crate::player_textures::SignedPlayerTextures; use crate::protocol::{BoundedInt, Nbt}; use crate::server::C2sPacketChannels; use crate::slotmap::{Key, SlotMap}; use crate::util::{chunks_in_view_distance, is_chunk_in_view_distance}; use crate::var_int::VarInt; use crate::{ - ident, ChunkPos, Chunks, DimensionId, Entities, EntityId, Server, SpatialIndex, Text, Ticks, - WorldMeta, LIBRARY_NAMESPACE, + ident, ChunkPos, Chunks, DimensionId, Entities, EntityId, NewClientData, Server, SpatialIndex, + Text, Ticks, WorldMeta, LIBRARY_NAMESPACE, }; pub struct Clients { @@ -120,8 +121,9 @@ pub struct Client { recv: Receiver, /// The tick this client was created. created_tick: Ticks, - username: String, uuid: Uuid, + username: String, + textures: Option, on_ground: bool, new_position: Vec3, old_position: Vec3, @@ -176,9 +178,8 @@ impl<'a> Deref for ClientMut<'a> { impl Client { pub(crate) fn new( packet_channels: C2sPacketChannels, - username: String, - uuid: Uuid, server: &Server, + ncd: NewClientData, ) -> Self { let (send, recv) = packet_channels; @@ -186,8 +187,9 @@ impl Client { send: Some(send), recv, created_tick: server.current_tick(), - username, - uuid, + uuid: ncd.uuid, + username: ncd.username, + textures: ncd.textures, on_ground: false, new_position: Vec3::default(), old_position: Vec3::default(), @@ -218,12 +220,16 @@ impl Client { self.created_tick } + pub fn uuid(&self) -> Uuid { + self.uuid + } + pub fn username(&self) -> &str { &self.username } - pub fn uuid(&self) -> Uuid { - self.uuid + pub fn textures(&self) -> Option<&SignedPlayerTextures> { + self.textures.as_ref() } pub fn position(&self) -> Vec3 { @@ -602,6 +608,9 @@ impl<'a> ClientMut<'a> { // Send the join game packet and other initial packets. We defer this until now // so that the user can set the client's location, game mode, etc. if self.created_tick == current_tick { + meta.player_list() + .initial_packets(|pkt| self.send_packet(pkt)); + self.send_packet(JoinGame { entity_id: 0, // EntityId 0 is reserved for clients. is_hardcore: false, // TODO @@ -621,7 +630,7 @@ impl<'a> ClientMut<'a> { max_players: VarInt(0), view_distance: BoundedInt(VarInt(self.new_max_view_distance as i32)), simulation_distance: VarInt(16), - reduced_debug_info: false, // TODO + reduced_debug_info: false, enable_respawn_screen: false, is_debug: false, is_flat: meta.is_flat(), @@ -630,9 +639,6 @@ impl<'a> ClientMut<'a> { .map(|(id, pos)| (ident!("{LIBRARY_NAMESPACE}:dimension_{}", id.0), pos)), }); - meta.player_list() - .initial_packets(|pkt| self.send_packet(pkt)); - self.teleport(self.position(), self.yaw(), self.pitch()); } else { if self.0.old_game_mode != self.0.new_game_mode { diff --git a/src/lib.rs b/src/lib.rs index 2509599..2d019e6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -31,6 +31,7 @@ pub mod util; mod var_int; mod var_long; pub mod world; +pub mod player_textures; pub use async_trait::async_trait; pub use biome::{Biome, BiomeId}; diff --git a/src/packets.rs b/src/packets.rs index 43435d6..b34fe70 100644 --- a/src/packets.rs +++ b/src/packets.rs @@ -329,6 +329,16 @@ mod private { pub trait Sealed {} } +def_struct! { + #[derive(PartialEq, Serialize, Deserialize)] + Property { + name: String, + value: String, + #[serde(skip_serializing_if = "Option::is_none")] + signature: Option + } +} + /// Packets and types used during the handshaking state. pub mod handshake { use super::*; @@ -413,14 +423,6 @@ pub mod login { } } - def_struct! { - Property { - name: String, - value: String, - signature: Option, - } - } - def_struct! { SetCompression 0x03 { threshold: VarInt @@ -474,7 +476,6 @@ pub mod play { pub mod s2c { use super::super::*; use crate::packets::login::c2s::SignatureData; - use crate::packets::login::s2c::Property; def_struct! { SpawnEntity 0x00 { diff --git a/src/player_list.rs b/src/player_list.rs index 10a38bf..d876fb5 100644 --- a/src/player_list.rs +++ b/src/player_list.rs @@ -3,15 +3,14 @@ use std::collections::{HashMap, HashSet}; use std::ops::Deref; use bitfield_struct::bitfield; -use serde::{Deserialize, Serialize}; -use url::Url; use uuid::Uuid; use crate::client::GameMode; -use crate::packets::login::s2c::Property; use crate::packets::play::s2c::{ PlayerInfo, PlayerInfoAddPlayer, PlayerListHeaderFooter, S2cPlayPacket, }; +use crate::packets::Property; +use crate::player_textures::SignedPlayerTextures; use crate::var_int::VarInt; use crate::Text; @@ -55,16 +54,11 @@ impl PlayerList { username: e.username.clone().into(), properties: { let mut properties = Vec::new(); - if e.skin().is_some() || e.cape().is_some() { - let textures = PlayerTextures { - skin: e.skin().cloned().map(TextureUrl::new), - cape: e.cape().cloned().map(TextureUrl::new), - }; - + if let Some(textures) = &e.textures { properties.push(Property { name: "textures".into(), - value: base64::encode(serde_json::to_string(&textures).unwrap()), - signature: None, + value: base64::encode(textures.payload()), + signature: Some(base64::encode(textures.signature())), }); } properties @@ -104,16 +98,11 @@ impl PlayerList { for (&uuid, e) in self.entries.iter() { if e.flags.created_this_tick() { let mut properties = Vec::new(); - if e.skin().is_some() || e.cape().is_some() { - let textures = PlayerTextures { - skin: e.skin().cloned().map(TextureUrl::new), - cape: e.cape().cloned().map(TextureUrl::new), - }; - + if let Some(textures) = &e.textures { properties.push(Property { name: "textures".into(), - value: base64::encode(serde_json::to_string(&textures).unwrap()), - signature: None, + value: base64::encode(textures.payload()), + signature: Some(base64::encode(textures.signature())), }); } @@ -186,51 +175,41 @@ impl<'a> PlayerListMut<'a> { PlayerListMut(self.0) } - pub fn add_player( + pub fn insert( &mut self, uuid: Uuid, username: impl Into, - skin: Option, - cape: Option, + textures: Option, game_mode: GameMode, ping: i32, display_name: impl Into>, ) { match self.0.entries.entry(uuid) { Entry::Occupied(mut oe) => { - let mut entry = PlayerListEntryMut(oe.get_mut()); + let mut e = PlayerListEntryMut(oe.get_mut()); let username = username.into(); - if entry.username() != username - || entry.skin() != skin.as_ref() - || entry.cape() != cape.as_ref() - { + if e.username() != username || e.textures != textures { self.0.removed.insert(*oe.key()); oe.insert(PlayerListEntry { username, - textures: PlayerTextures { - skin: skin.map(TextureUrl::new), - cape: cape.map(TextureUrl::new), - }, + textures, game_mode, ping, display_name: display_name.into(), flags: EntryFlags::new().with_created_this_tick(true), }); } else { - entry.set_game_mode(game_mode); - entry.set_ping(ping); - entry.set_display_name(display_name); + e.set_game_mode(game_mode); + e.set_ping(ping); + e.set_display_name(display_name); } } Entry::Vacant(ve) => { ve.insert(PlayerListEntry { username: username.into(), - textures: PlayerTextures { - skin: skin.map(TextureUrl::new), - cape: cape.map(TextureUrl::new), - }, + textures, game_mode, ping, display_name: display_name.into(), @@ -240,7 +219,7 @@ impl<'a> PlayerListMut<'a> { } } - pub fn remove_player(&mut self, uuid: Uuid) -> bool { + pub fn remove(&mut self, uuid: Uuid) -> bool { if self.0.entries.remove(&uuid).is_some() { self.0.removed.insert(uuid); true @@ -283,7 +262,7 @@ impl<'a> PlayerListMut<'a> { pub struct PlayerListEntry { username: String, - textures: PlayerTextures, + textures: Option, game_mode: GameMode, ping: i32, display_name: Option, @@ -295,12 +274,8 @@ impl PlayerListEntry { &self.username } - pub fn skin(&self) -> Option<&Url> { - self.textures.skin.as_ref().map(|t| &t.url) - } - - pub fn cape(&self) -> Option<&Url> { - self.textures.cape.as_ref().map(|t| &t.url) + pub fn textures(&self) -> Option<&SignedPlayerTextures> { + self.textures.as_ref() } pub fn game_mode(&self) -> GameMode { @@ -363,23 +338,3 @@ struct EntryFlags { #[bits(4)] _pad: u8, } - -#[derive(Clone, PartialEq, Debug, Serialize, Deserialize)] -#[serde(rename_all = "UPPERCASE")] -pub(crate) struct PlayerTextures { - #[serde(default, skip_serializing_if = "Option::is_none")] - skin: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - cape: Option, -} - -#[derive(Clone, PartialEq, Debug, Serialize, Deserialize)] -struct TextureUrl { - url: Url, -} - -impl TextureUrl { - fn new(url: Url) -> Self { - Self { url } - } -} diff --git a/src/player_textures.rs b/src/player_textures.rs new file mode 100644 index 0000000..b8507cd --- /dev/null +++ b/src/player_textures.rs @@ -0,0 +1,76 @@ +use anyhow::Context; +use serde::{Deserialize, Serialize}; +use url::Url; + +#[derive(Clone, PartialEq, Debug)] +pub struct SignedPlayerTextures { + payload: Box<[u8]>, + signature: Box<[u8]>, +} + +impl SignedPlayerTextures { + pub fn payload(&self) -> &[u8] { + &self.payload + } + + pub fn signature(&self) -> &[u8] { + &self.signature + } + + pub fn to_textures(&self) -> PlayerTextures { + self.to_textures_fallible() + .expect("payload should have been validated earlier") + } + + fn to_textures_fallible(&self) -> anyhow::Result { + #[derive(Debug, Deserialize)] + struct Textures { + textures: PlayerTexturesPayload, + } + + let textures: Textures = serde_json::from_slice(&self.payload)?; + + Ok(PlayerTextures { + skin: textures.textures.skin.map(|t| t.url), + cape: textures.textures.cape.map(|t| t.url), + }) + } + + pub(crate) fn from_base64(payload: String, signature: String) -> anyhow::Result { + let res = Self { + payload: base64::decode(payload)?.into_boxed_slice(), + signature: base64::decode(signature)?.into_boxed_slice(), + }; + + match res.to_textures_fallible() { + Ok(_) => Ok(res), + Err(e) => Err(e).context("failed to parse textures payload"), + } + } +} + +#[derive(Clone, PartialEq, Default, Debug)] +pub struct PlayerTextures { + pub skin: Option, + pub cape: Option, +} + +impl From for PlayerTextures { + fn from(spt: SignedPlayerTextures) -> Self { + spt.to_textures() + } +} + +#[derive(Clone, PartialEq, Debug, Serialize, Deserialize)] +#[serde(rename_all = "UPPERCASE")] +pub struct PlayerTexturesPayload { + #[serde(default, skip_serializing_if = "Option::is_none")] + skin: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + cape: Option, +} + +#[derive(Clone, PartialEq, Debug, Serialize, Deserialize)] +struct TextureUrl { + url: Url, +} diff --git a/src/server.rs b/src/server.rs index 36b7fd7..4160400 100644 --- a/src/server.rs +++ b/src/server.rs @@ -29,14 +29,14 @@ use uuid::Uuid; use crate::codec::{Decoder, Encoder}; use crate::config::{Config, ServerListPing}; use crate::packets::handshake::{Handshake, HandshakeNextState}; -use crate::packets::login; use crate::packets::login::c2s::{EncryptionResponse, LoginStart, VerifyTokenOrMsgSig}; use crate::packets::login::s2c::{EncryptionRequest, LoginSuccess, SetCompression}; use crate::packets::play::c2s::C2sPlayPacket; use crate::packets::play::s2c::S2cPlayPacket; use crate::packets::status::c2s::{Ping, Request}; use crate::packets::status::s2c::{Pong, Response}; -use crate::player_list::PlayerTextures; +use crate::packets::{login, Property}; +use crate::player_textures::SignedPlayerTextures; use crate::protocol::{BoundedArray, BoundedString}; use crate::util::valid_username; use crate::var_int::VarInt; @@ -90,6 +90,7 @@ struct ServerInner { pub struct NewClientData { pub uuid: Uuid, pub username: String, + pub textures: Option, pub remote_addr: SocketAddr, } @@ -414,17 +415,12 @@ fn join_player(server: &Server, mut worlds: WorldsMut, msg: NewClientMessage) { let (clientbound_tx, clientbound_rx) = flume::bounded(server.0.outgoing_packet_capacity); let (serverbound_tx, serverbound_rx) = flume::bounded(server.0.incoming_packet_capacity); - let client_packet_channels: S2cPacketChannels = (serverbound_tx, clientbound_rx); - let server_packet_channels: C2sPacketChannels = (clientbound_tx, serverbound_rx); + let s2c_packet_channels: S2cPacketChannels = (serverbound_tx, clientbound_rx); + let c2s_packet_channels: C2sPacketChannels = (clientbound_tx, serverbound_rx); - let _ = msg.reply.send(client_packet_channels); + let _ = msg.reply.send(s2c_packet_channels); - let mut client = Client::new( - server_packet_channels, - msg.ncd.username, - msg.ncd.uuid, - server, - ); + let mut client = Client::new(c2s_packet_channels, server, msg.ncd); let mut client_mut = ClientMut::new(&mut client); match server @@ -641,12 +637,6 @@ async fn handle_login( properties: Vec, } - #[derive(Debug, Deserialize)] - struct Property { - name: String, - value: String, - } - let hash = Sha1::new() .chain(&shared_secret) .chain(&server.0.public_key_der) @@ -670,8 +660,11 @@ async fn handle_login( let uuid = Uuid::parse_str(&data.id).context("failed to parse player's UUID")?; let textures = match data.properties.into_iter().find(|p| p.name == "textures") { - Some(p) => decode_textures(p.value).context("failed to decode skin blob")?, - None => bail!("failed to find skin blob in auth response"), + Some(p) => SignedPlayerTextures::from_base64( + p.value, + p.signature.context("missing signature for textures")?, + )?, + None => bail!("failed to find textures in auth response"), }; (uuid, Some(textures)) @@ -694,6 +687,7 @@ async fn handle_login( let npd = NewClientData { uuid, username, + textures, remote_addr, }; @@ -713,17 +707,6 @@ async fn handle_login( Ok(Some(npd)) } -fn decode_textures(encoded: String) -> anyhow::Result { - #[derive(Debug, Deserialize)] - struct Textures { - textures: PlayerTextures, - } - - let bytes = base64::decode(encoded)?; - let textures: Textures = serde_json::from_slice(&bytes)?; - Ok(textures.textures) -} - async fn handle_play(server: &Server, c: Codec, ncd: NewClientData) -> anyhow::Result<()> { let (reply_tx, reply_rx) = oneshot::channel();