mirror of
https://github.com/italicsjenga/valence.git
synced 2024-12-23 22:41:30 +11:00
Don't tamper with the texture payload
The texture payload (skin + cape URL) that we get from the auth server needs to stay intact so the signature is not invalidated. However, skins still aren't loading. Not sure what's up with that.
This commit is contained in:
parent
055dd03ffc
commit
e97df76a75
|
@ -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() {
|
||||
|
|
|
@ -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,
|
||||
);
|
||||
|
|
|
@ -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<C2sPlayPacket>,
|
||||
/// The tick this client was created.
|
||||
created_tick: Ticks,
|
||||
username: String,
|
||||
uuid: Uuid,
|
||||
username: String,
|
||||
textures: Option<SignedPlayerTextures>,
|
||||
on_ground: bool,
|
||||
new_position: Vec3<f64>,
|
||||
old_position: Vec3<f64>,
|
||||
|
@ -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<f64> {
|
||||
|
@ -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 {
|
||||
|
|
|
@ -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};
|
||||
|
|
|
@ -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<String>
|
||||
}
|
||||
}
|
||||
|
||||
/// 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<String>,
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
|
|
|
@ -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<String>,
|
||||
skin: Option<Url>,
|
||||
cape: Option<Url>,
|
||||
textures: Option<SignedPlayerTextures>,
|
||||
game_mode: GameMode,
|
||||
ping: i32,
|
||||
display_name: impl Into<Option<Text>>,
|
||||
) {
|
||||
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<SignedPlayerTextures>,
|
||||
game_mode: GameMode,
|
||||
ping: i32,
|
||||
display_name: Option<Text>,
|
||||
|
@ -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<TextureUrl>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
cape: Option<TextureUrl>,
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Debug, Serialize, Deserialize)]
|
||||
struct TextureUrl {
|
||||
url: Url,
|
||||
}
|
||||
|
||||
impl TextureUrl {
|
||||
fn new(url: Url) -> Self {
|
||||
Self { url }
|
||||
}
|
||||
}
|
||||
|
|
76
src/player_textures.rs
Normal file
76
src/player_textures.rs
Normal file
|
@ -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<PlayerTextures> {
|
||||
#[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<Self> {
|
||||
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<Url>,
|
||||
pub cape: Option<Url>,
|
||||
}
|
||||
|
||||
impl From<SignedPlayerTextures> 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<TextureUrl>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
cape: Option<TextureUrl>,
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Debug, Serialize, Deserialize)]
|
||||
struct TextureUrl {
|
||||
url: Url,
|
||||
}
|
|
@ -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<SignedPlayerTextures>,
|
||||
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<Property>,
|
||||
}
|
||||
|
||||
#[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<PlayerTextures> {
|
||||
#[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();
|
||||
|
||||
|
|
Loading…
Reference in a new issue