diff --git a/CHANGELOG.md b/CHANGELOG.md index b8d1c9a5c2..ed652da237 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - Server kicks old client when a user is trying to log in again (often the case when a user's original connection gets dropped) +- Client has better detection for incompatible versions - Added a raycast check to beams to prevent their effect applying through walls ## [0.9.0] - 2021-03-20 diff --git a/assets/voxygen/i18n/en/main.ron b/assets/voxygen/i18n/en/main.ron index ac9adf55ab..bb51c1cdd0 100644 --- a/assets/voxygen/i18n/en/main.ron +++ b/assets/voxygen/i18n/en/main.ron @@ -45,7 +45,8 @@ https://veloren.net/account/."#, "main.login.insecure_auth_scheme": "The auth Scheme HTTP is NOT supported. It's insecure! For development purposes, HTTP is allowed for 'localhost' or debug builds", "main.login.server_full": "Server is full", "main.login.untrusted_auth_server": "Auth server not trusted", - "main.login.outdated_client_or_server": "ServerWentMad: Probably versions are incompatible, check for updates.", + "main.login.prob_outdated_client_or_server": "ServerWentMad: Probably versions are incompatible, check for updates.", + "main.login.outdated_client_or_server": "Version check failed: versions are incompatible, check for updates!", "main.login.timeout": "Timeout: Server did not respond in time. (Overloaded or network issues).", "main.login.server_shut_down": "Server shut down", "main.login.already_logged_in": "You are already logged into the server.", diff --git a/client/src/error.rs b/client/src/error.rs index 02905f585d..23140a747d 100644 --- a/client/src/error.rs +++ b/client/src/error.rs @@ -8,6 +8,7 @@ pub enum Error { NetworkErr(NetworkError), ParticipantErr(ParticipantError), StreamErr(StreamError), + ServerValidationFailed(String, String), ServerWentMad, ServerTimeout, ServerShutdown, diff --git a/client/src/lib.rs b/client/src/lib.rs index 4066c36bc4..b5b5b25b77 100644 --- a/client/src/lib.rs +++ b/client/src/lib.rs @@ -5,6 +5,7 @@ pub mod addr; pub mod cmd; pub mod error; +pub mod validate_version; // Reexports pub use crate::error::Error; @@ -225,6 +226,7 @@ impl Client { let character_screen_stream = participant.opened().await?; let in_game_stream = participant.opened().await?; let terrain_stream = participant.opened().await?; + let validation_stream = participant.opened().await?; register_stream.send(ClientType::Game)?; let server_info: ServerInfo = register_stream.recv().await?; @@ -243,6 +245,22 @@ impl Client { ping_stream.send(PingMsg::Ping)?; + // Validate Server dump + if let Err(()) = validate_version::validate_dump_from_server(validation_stream).await { + error!( + "Client does not pass server dump validation, versions do not match. \ + Install/Update the right version" + ); + return Err(Error::ServerValidationFailed( + format!("{}[{}]", server_info.git_hash, server_info.git_date), + format!( + "{}[{}]", + common::util::GIT_HASH.to_string(), + common::util::GIT_DATE.to_string() + ), + )); + }; + // Wait for initial sync let mut ping_interval = tokio::time::interval(core::time::Duration::from_secs(1)); let ( diff --git a/client/src/validate_version.rs b/client/src/validate_version.rs new file mode 100644 index 0000000000..c9b7bd710d --- /dev/null +++ b/client/src/validate_version.rs @@ -0,0 +1,145 @@ +use tracing::{debug, trace}; + +/// assume deserialization is exactly what we hardcoded. +/// When it fails, we can generate a wrong version error +/// message and people stop annoying xMAC94x +/// see src/server/connecton_handler.rs +pub(crate) async fn validate_dump_from_server( + mut validate_stream: network::Stream, +) -> Result<(), ()> { + use common::{ + character::Character, + comp::{ + inventory::Inventory, + item::{tool::ToolKind, Reagent}, + skills::SkillGroupKind, + }, + outcome::Outcome, + terrain::{biome::BiomeKind, TerrainChunkMeta}, + trade::{Good, PendingTrade, Trades}, + uid::Uid, + }; + use common_net::msg::{world_msg::EconomyInfo, ServerGeneral}; + use std::collections::HashMap; + use vek::*; + + trace!("check Character (1)"); + match validate_stream.recv::().await { + Ok(ServerGeneral::CharacterListUpdate(vec)) => { + check_eq(&vec.len(), 1)?; + let item = &vec[0]; + check_eq(&item.character, Character { + id: Some(1337), + alias: "foobar".to_owned(), + })?; + check(item.body.is_humanoid())?; + let inv = Inventory::new_empty() + .equipped_items() + .cloned() + .collect::>(); + let inv_remote = item.inventory.equipped_items().cloned().collect::>(); + check_eq(&inv_remote, inv)?; + }, + _ => return Err(()), + } + + trace!("check Outcomes (2)"); + match validate_stream.recv::().await { + Ok(ServerGeneral::Outcomes(vec)) => { + check_eq(&vec.len(), 3)?; + check_eq(&vec[0], Outcome::Explosion { + pos: Vec3::new(1.0, 2.0, 3.0), + power: 4.0, + radius: 5.0, + is_attack: true, + reagent: Some(Reagent::Blue), + })?; + check_eq(&vec[1], Outcome::SkillPointGain { + uid: Uid::from(1337u64), + pos: Vec3::new(2.0, 4.0, 6.0), + skill_tree: SkillGroupKind::Weapon(ToolKind::Empty), + total_points: 99, + })?; + check_eq(&vec[2], Outcome::BreakBlock { + pos: Vec3::new(1, 2, 3), + color: Some(Rgb::new(0u8, 8u8, 13u8)), + })?; + }, + _ => return Err(()), + } + + trace!("check Terrain (3)"); + match validate_stream.recv::().await { + Ok(ServerGeneral::TerrainChunkUpdate { key, chunk }) => { + check_eq(&key, Vec2::new(42, 1337))?; + let chunk = chunk?; + check_eq(chunk.meta(), TerrainChunkMeta::void())?; + check_eq(&chunk.get_min_z(), 5)?; + }, + _ => return Err(()), + } + + trace!("check Componity Sync (4)"); + match validate_stream.recv::().await { + Ok(ServerGeneral::CompSync(item)) => { + check_eq(&item.comp_updates.len(), 2)?; + //check_eq(&item.comp_updates[0], + // CompUpdateKind::Inserted(comp::Pos(Vec3::new(42.1337, 0.0, + // 0.0))))?; check_eq(&item.comp_updates[1], + // CompUpdateKind::Inserted(comp::Pos(Vec3::new(0.0, 42.1337, + // 0.0))))?; + }, + _ => return Err(()), + } + + trace!("check Pending Trade (5)"); + match validate_stream.recv::().await { + Ok(ServerGeneral::UpdatePendingTrade(tradeid, pending)) => { + let uid = Uid::from(70); + check_eq(&tradeid, Trades::default().begin_trade(uid, uid))?; + check_eq(&pending, PendingTrade::new(uid, Uid::from(71)))?; + }, + _ => return Err(()), + } + + trace!("check Economy (6)"); + match validate_stream.recv::().await { + Ok(ServerGeneral::SiteEconomy(info)) => { + check_eq(&info, EconomyInfo { + id: 99, + population: 55, + stock: vec![ + (Good::Wood, 50.0), + (Good::Tools, 33.3), + (Good::Coin, 9000.1), + ] + .into_iter() + .collect::>(), + labor_values: HashMap::new(), + values: vec![ + (Good::RoadSecurity, 1.0), + (Good::Terrain(BiomeKind::Forest), 1.0), + ] + .into_iter() + .collect::>(), + labors: Vec::new(), + last_exports: HashMap::new(), + resources: HashMap::new(), + })?; + }, + _ => return Err(()), + } + + Ok(()) +} + +fn check_eq(one: &T, two: T) -> Result<(), ()> { + if one == &two { + Ok(()) + } else { + debug!(?one, ?two, "failed check"); + Err(()) + } +} + +fn check(b: bool) -> Result<(), ()> { if b { Ok(()) } else { Err(()) } } diff --git a/common/net/src/msg/world_msg.rs b/common/net/src/msg/world_msg.rs index 12b0c46f61..1d80447951 100644 --- a/common/net/src/msg/world_msg.rs +++ b/common/net/src/msg/world_msg.rs @@ -145,7 +145,7 @@ pub enum SiteKind { Tree, } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub struct EconomyInfo { pub id: SiteId, pub population: u32, diff --git a/common/src/outcome.rs b/common/src/outcome.rs index 56cc92979c..b2ed45badd 100644 --- a/common/src/outcome.rs +++ b/common/src/outcome.rs @@ -8,7 +8,7 @@ use vek::*; /// occur, nor is it something that may be cancelled or otherwise altered. Its /// primary purpose is to act as something for frontends (both server and /// client) to listen to in order to receive feedback about events in the world. -#[derive(Clone, Debug, Serialize, Deserialize)] +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] pub enum Outcome { Explosion { pos: Vec3, diff --git a/common/src/terrain/mod.rs b/common/src/terrain/mod.rs index 5360b6c7c8..61ce8ce4da 100644 --- a/common/src/terrain/mod.rs +++ b/common/src/terrain/mod.rs @@ -78,7 +78,7 @@ impl TerrainChunkSize { // TerrainChunkMeta -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub struct TerrainChunkMeta { name: Option, biome: BiomeKind, diff --git a/server/src/connection_handler.rs b/server/src/connection_handler.rs index 44b975ec04..3a3ddd89ac 100644 --- a/server/src/connection_handler.rs +++ b/server/src/connection_handler.rs @@ -88,6 +88,121 @@ impl ConnectionHandler { } } + /// This code just generates some messages so that the client can assume + /// deserialization This is completely random chosen with the goal to + /// cover as much as possible and to stop xMAC94x being annoyed in + /// discord by people that just have a old version + fn dump_messages_so_client_can_validate_itself( + mut validate_stream: network::Stream, + ) -> Result<(), network::StreamError> { + use common::{ + character::{Character, CharacterItem}, + comp, + comp::{ + humanoid, + inventory::Inventory, + item::{tool::ToolKind, Reagent}, + skills::SkillGroupKind, + }, + outcome::Outcome, + terrain::{biome::BiomeKind, Block, BlockKind, TerrainChunk, TerrainChunkMeta}, + trade::{Good, PendingTrade, Trades}, + uid::Uid, + }; + use common_net::{ + msg::{world_msg::EconomyInfo, EcsCompPacket, ServerGeneral}, + sync::CompSyncPackage, + }; + use rand::SeedableRng; + use std::collections::HashMap; + use vek::*; + + // 1. Simulate Character + let mut rng = rand::rngs::SmallRng::from_seed([42; 32]); + let item = CharacterItem { + character: Character { + id: Some(1337), + alias: "foobar".to_owned(), + }, + body: comp::Body::Humanoid(humanoid::Body::random_with( + &mut rng, + &humanoid::Species::Undead, + )), + inventory: Inventory::new_empty(), + }; + validate_stream.send(ServerGeneral::CharacterListUpdate(vec![item]))?; + + // 2. Simulate Outcomes + let item1 = Outcome::Explosion { + pos: Vec3::new(1.0, 2.0, 3.0), + power: 4.0, + radius: 5.0, + is_attack: true, + reagent: Some(Reagent::Blue), + }; + let item2 = Outcome::SkillPointGain { + uid: Uid::from(1337u64), + pos: Vec3::new(2.0, 4.0, 6.0), + skill_tree: SkillGroupKind::Weapon(ToolKind::Empty), + total_points: 99, + }; + let item3 = Outcome::BreakBlock { + pos: Vec3::new(1, 2, 3), + color: Some(Rgb::new(0u8, 8u8, 13u8)), + }; + validate_stream.send(ServerGeneral::Outcomes(vec![item1, item2, item3]))?; + + // 3. Simulate Terrain + let item = TerrainChunk::new( + 5, + Block::new(BlockKind::Water, Rgb::zero()), + Block::new(BlockKind::Air, Rgb::zero()), + TerrainChunkMeta::void(), + ); + validate_stream.send(ServerGeneral::TerrainChunkUpdate { + key: Vec2::new(42, 1337), + chunk: Ok(Box::new(item)), + })?; + + // 4. Simulate Componity Sync + let mut item = CompSyncPackage::::new(); + let uid = Uid::from(70); + item.comp_inserted(uid, comp::Pos(Vec3::new(42.1337, 0.0, 0.0))); + item.comp_inserted(uid, comp::Vel(Vec3::new(0.0, 42.1337, 0.0))); + validate_stream.send(ServerGeneral::CompSync(item))?; + + // 5. Pending Trade + let uid = Uid::from(70); + validate_stream.send(ServerGeneral::UpdatePendingTrade( + Trades::default().begin_trade(uid, uid), + PendingTrade::new(uid, Uid::from(71)), + ))?; + + // 6. Economy Info + validate_stream.send(ServerGeneral::SiteEconomy(EconomyInfo { + id: 99, + population: 55, + stock: vec![ + (Good::Wood, 50.0), + (Good::Tools, 33.3), + (Good::Coin, 9000.1), + ] + .into_iter() + .collect::>(), + labor_values: HashMap::new(), + values: vec![ + (Good::RoadSecurity, 1.0), + (Good::Terrain(BiomeKind::Forest), 1.0), + ] + .into_iter() + .collect::>(), + labors: Vec::new(), + last_exports: HashMap::new(), + resources: HashMap::new(), + }))?; + Ok(()) + } + async fn init_participant( participant: Participant, client_sender: Sender, @@ -106,6 +221,7 @@ impl ConnectionHandler { let character_screen_stream = participant.open(3, reliablec, 500).await?; let in_game_stream = participant.open(3, reliablec, 100_000).await?; let terrain_stream = participant.open(4, reliablec, 20_000).await?; + let validate_stream = participant.open(4, reliablec, 1_000_000).await?; let server_data = receiver.recv()?; @@ -123,6 +239,11 @@ impl ConnectionHandler { Some(client_type) => client_type?, }; + if let Err(e) = Self::dump_messages_so_client_can_validate_itself(validate_stream) { + trace!(?e, ?client_type, "a client dropped as he failed validation"); + return Err(e.into()); + }; + let client = Client::new( client_type, participant, diff --git a/voxygen/src/menu/main/mod.rs b/voxygen/src/menu/main/mod.rs index af82105301..0456395f18 100644 --- a/voxygen/src/menu/main/mod.rs +++ b/voxygen/src/menu/main/mod.rs @@ -139,8 +139,14 @@ impl PlayState for MainMenuState { client::Error::AuthServerNotTrusted => localized_strings .get("main.login.untrusted_auth_server") .into(), + client::Error::ServerValidationFailed(server, client) => format!( + "{}: server version {}, client version {}", + localized_strings.get("main.login.outdated_client_or_server"), + server, + client, + ), client::Error::ServerWentMad => localized_strings - .get("main.login.outdated_client_or_server") + .get("main.login.prob_outdated_client_or_server") .into(), client::Error::ServerTimeout => { localized_strings.get("main.login.timeout").into()