# HG changeset patch # User alfadur # Date 1740242371 -10800 # Node ID 5febd2bc5372c734c90e85d45664773ff2700f1e # Parent 278533359a93e4f230ca388c8735158e20238973 Add server reconnection tokens and anteroom local list of used nicks diff -r 278533359a93 -r 5febd2bc5372 rust/hedgewars-network-protocol/src/messages.rs --- a/rust/hedgewars-network-protocol/src/messages.rs Mon Feb 17 16:38:24 2025 +0100 +++ b/rust/hedgewars-network-protocol/src/messages.rs Sat Feb 22 19:39:31 2025 +0300 @@ -1,6 +1,8 @@ use crate::types::{GameCfg, ServerVar, TeamInfo, VoteType}; use std::iter::once; +//todo!("add help message") + #[derive(PartialEq, Eq, Clone, Debug)] pub enum HwProtocolMessage { // common messages @@ -13,7 +15,7 @@ SuperPower, Info(String), // anteroom messages - Nick(String), + Nick(String, Option), Proto(u16), Password(String, String), Checker(u16, String, String), @@ -52,6 +54,7 @@ Delegate(String), TeamChat(String), MaxTeams(u8), + //command line messages Fix, Unfix, Greeting(Option), @@ -120,6 +123,7 @@ Bye(String), Nick(String), + Token(String), Proto(u16), AskPassword(String), ServerAuth(String), @@ -281,7 +285,8 @@ ToggleServerRegisteredOnly => msg!["CMD", "REGISTERED_ONLY"], SuperPower => msg!["CMD", "SUPER_POWER"], Info(info) => msg!["CMD", format!("INFO {}", info)], - Nick(nick) => msg!("NICK", nick), + Nick(nick, None) => msg!["NICK", nick], + Nick(nick, Some(token)) => msg!["NICK", nick, token], Proto(version) => msg!["PROTO", version], Password(p, s) => msg!["PASSWORD", p, s], Checker(i, n, p) => msg!["CHECKER", i, n, p], @@ -381,6 +386,7 @@ Redirect(port) => msg!["REDIRECT", port], Bye(msg) => msg!["BYE", msg], Nick(nick) => msg!["NICK", nick], + Token(token) => msg!["TOKEN", token], Proto(proto) => msg!["PROTO", proto], AskPassword(salt) => msg!["ASKPASSWORD", salt], ServerAuth(hash) => msg!["SERVER_AUTH", hash], diff -r 278533359a93 -r 5febd2bc5372 rust/hedgewars-network-protocol/src/parser.rs --- a/rust/hedgewars-network-protocol/src/parser.rs Mon Feb 17 16:38:24 2025 +0100 +++ b/rust/hedgewars-network-protocol/src/parser.rs Sat Feb 22 19:39:31 2025 +0300 @@ -220,7 +220,6 @@ } alt(( - message("NICK\n", a_line, Nick), message("INFO\n", a_line, Info), message("CHAT\n", a_line, Chat), message("PART", opt_arg, Part), @@ -496,6 +495,10 @@ ), |values| CheckedOk(values.unwrap_or_default()), ), + preceded( + tag("NICK\n"), + map(pair(a_line, opt_arg), |(nick, token)| Nick(nick, token)), + ), ))(input) } diff -r 278533359a93 -r 5febd2bc5372 rust/hedgewars-network-protocol/src/tests/parser.rs --- a/rust/hedgewars-network-protocol/src/tests/parser.rs Mon Feb 17 16:38:24 2025 +0100 +++ b/rust/hedgewars-network-protocol/src/tests/parser.rs Sat Feb 22 19:39:31 2025 +0300 @@ -22,7 +22,7 @@ assert_eq!(message(b"START_GAME\n\n"), Ok((&b""[..], StartGame))); assert_eq!( message(b"NICK\nit's me\n\n"), - Ok((&b""[..], Nick("it's me".to_string()))) + Ok((&b""[..], Nick("it's me".to_string(), None))) ); assert_eq!(message(b"PROTO\n51\n\n"), Ok((&b""[..], Proto(51)))); assert_eq!( diff -r 278533359a93 -r 5febd2bc5372 rust/hedgewars-network-protocol/src/tests/test.rs --- a/rust/hedgewars-network-protocol/src/tests/test.rs Mon Feb 17 16:38:24 2025 +0100 +++ b/rust/hedgewars-network-protocol/src/tests/test.rs Sat Feb 22 19:39:31 2025 +0300 @@ -206,7 +206,7 @@ 6 => ToggleServerRegisteredOnly(), 7 => SuperPower(), 8 => Info(Ascii), - 9 => Nick(Ascii), + 9 => Nick(Ascii, Option), 10 => Proto(u16), 11 => Password(Ascii, Ascii), 12 => Checker(u16, Ascii, Ascii), @@ -264,7 +264,7 @@ pub fn gen_server_msg() -> BoxedStrategy where { use HwServerMessage::*; - let res = (0..=38).no_shrink().prop_flat_map(|i| { + let res = (0..=39).no_shrink().prop_flat_map(|i| { proto_msg_match!(i, def = Ping, 0 => Connected(Ascii, u32), 1 => Redirect(u16), @@ -304,7 +304,8 @@ 35 => Notice(Ascii), 36 => Warning(Ascii), 37 => Error(Ascii), - 38 => Replay(Vec) + 38 => Replay(Vec), + 39 => Token(Ascii) ) }); res.boxed() diff -r 278533359a93 -r 5febd2bc5372 rust/hedgewars-server/src/core.rs --- a/rust/hedgewars-server/src/core.rs Mon Feb 17 16:38:24 2025 +0100 +++ b/rust/hedgewars-server/src/core.rs Sat Feb 22 19:39:31 2025 +0300 @@ -1,5 +1,6 @@ pub mod anteroom; pub mod client; +pub mod digest; pub mod indexslab; pub mod room; pub mod server; diff -r 278533359a93 -r 5febd2bc5372 rust/hedgewars-server/src/core/anteroom.rs --- a/rust/hedgewars-server/src/core/anteroom.rs Mon Feb 17 16:38:24 2025 +0100 +++ b/rust/hedgewars-server/src/core/anteroom.rs Sat Feb 22 19:39:31 2025 +0300 @@ -1,5 +1,8 @@ use super::{indexslab::IndexSlab, types::ClientId}; +use crate::core::client::HwClient; +use crate::core::digest::Sha1Digest; use chrono::{offset, DateTime}; +use std::collections::{HashMap, HashSet}; use std::{iter::Iterator, num::NonZeroU16}; pub struct HwAnteroomClient { @@ -51,8 +54,10 @@ } pub struct HwAnteroom { - pub clients: IndexSlab, + clients: IndexSlab, bans: BanCollection, + taken_nicks: HashSet, + reconnection_tokens: HashMap, } impl HwAnteroom { @@ -61,6 +66,8 @@ HwAnteroom { clients, bans: BanCollection::new(), + taken_nicks: Default::default(), + reconnection_tokens: Default::default(), } } @@ -82,8 +89,54 @@ self.clients.insert(client_id, client); } + pub fn has_client(&self, id: ClientId) -> bool { + self.clients.contains(id) + } + + pub fn get_client(&mut self, id: ClientId) -> &HwAnteroomClient { + &self.clients[id] + } + + pub fn get_client_mut(&mut self, id: ClientId) -> &mut HwAnteroomClient { + &mut self.clients[id] + } + pub fn remove_client(&mut self, client_id: ClientId) -> Option { let client = self.clients.remove(client_id); + if let Some(HwAnteroomClient { + nick: Some(nick), .. + }) = &client + { + self.taken_nicks.remove(nick); + } client } + + pub fn nick_taken(&self, nick: &str) -> bool { + self.taken_nicks.contains(nick) + } + + pub fn remember_nick(&mut self, nick: String) { + self.taken_nicks.insert(nick); + } + + pub fn forget_nick(&mut self, nick: &str) { + self.taken_nicks.remove(nick); + } + + #[inline] + pub fn get_nick_token(&self, nick: &str) -> Option<&str> { + self.reconnection_tokens.get(nick).map(|s| &s[..]) + } + + #[inline] + pub fn register_nick_token(&mut self, nick: &str) -> Option<&str> { + if self.reconnection_tokens.contains_key(nick) { + None + } else { + let token = format!("{:x}", Sha1Digest::random()); + self.reconnection_tokens.insert(nick.to_string(), token); + Some(&self.reconnection_tokens[nick]) + } + } } diff -r 278533359a93 -r 5febd2bc5372 rust/hedgewars-server/src/core/client.rs --- a/rust/hedgewars-server/src/core/client.rs Mon Feb 17 16:38:24 2025 +0100 +++ b/rust/hedgewars-server/src/core/client.rs Sat Feb 22 19:39:31 2025 +0300 @@ -1,8 +1,9 @@ use super::types::ClientId; use bitflags::*; +use std::ops::Deref; bitflags! { - pub struct ClientFlags: u8 { + pub struct ClientFlags: u16 { const IS_ADMIN = 0b0000_0001; const IS_MASTER = 0b0000_0010; const IS_READY = 0b0000_0100; @@ -11,6 +12,7 @@ const HAS_SUPER_POWER = 0b0010_0000; const IS_REGISTERED = 0b0100_0000; const IS_MODERATOR = 0b1000_0000; + const IS_REJOINED = 0b1_0000_0000; const NONE = 0b0000_0000; const DEFAULT = Self::NONE.bits; @@ -72,6 +74,9 @@ pub fn is_registered(&self) -> bool { self.contains(ClientFlags::IS_REGISTERED) } + pub fn is_rejoined(&self) -> bool { + self.contains(ClientFlags::IS_REJOINED) + } pub fn set_is_admin(&mut self, value: bool) { self.set(ClientFlags::IS_ADMIN, value) @@ -94,4 +99,7 @@ pub fn set_is_registered(&mut self, value: bool) { self.set(ClientFlags::IS_REGISTERED, value) } + pub fn set_is_rejoined(&mut self, value: bool) { + self.set(ClientFlags::IS_REJOINED, value) + } } diff -r 278533359a93 -r 5febd2bc5372 rust/hedgewars-server/src/core/digest.rs --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/rust/hedgewars-server/src/core/digest.rs Sat Feb 22 19:39:31 2025 +0300 @@ -0,0 +1,55 @@ +use rand::{thread_rng, RngCore}; +use std::fmt::{Formatter, LowerHex}; + +#[derive(PartialEq, Debug)] +pub struct Sha1Digest([u8; 20]); + +impl Sha1Digest { + pub fn new(digest: [u8; 20]) -> Self { + Self(digest) + } + + pub fn random() -> Self { + let mut result = Sha1Digest(Default::default()); + thread_rng().fill_bytes(&mut result.0); + result + } +} + +impl LowerHex for Sha1Digest { + fn fmt(&self, f: &mut Formatter) -> Result<(), std::fmt::Error> { + for byte in &self.0 { + write!(f, "{:02x}", byte)?; + } + Ok(()) + } +} + +impl PartialEq<&str> for Sha1Digest { + fn eq(&self, other: &&str) -> bool { + if other.len() != self.0.len() * 2 { + false + } else { + #[inline] + fn convert(c: u8) -> u8 { + if c > b'9' { + c.wrapping_sub(b'a').saturating_add(10) + } else { + c.wrapping_sub(b'0') + } + } + + other + .as_bytes() + .chunks_exact(2) + .zip(&self.0) + .all(|(chars, byte)| { + if let [hi, lo] = chars { + convert(*lo) == byte & 0x0f && convert(*hi) == (byte & 0xf0) >> 4 + } else { + unreachable!() + } + }) + } + } +} diff -r 278533359a93 -r 5febd2bc5372 rust/hedgewars-server/src/core/server.rs --- a/rust/hedgewars-server/src/core/server.rs Mon Feb 17 16:38:24 2025 +0100 +++ b/rust/hedgewars-server/src/core/server.rs Sat Feb 22 19:39:31 2025 +0300 @@ -11,10 +11,10 @@ use crate::server::replaystorage::ReplayStorage; use bitflags::*; -use log::*; -use rand::{self, seq::SliceRandom, thread_rng, Rng}; +use rand::{self, thread_rng, Rng}; use slab::Slab; -use std::{borrow::BorrowMut, cmp::min, collections::HashSet, iter, mem::replace}; +use std::collections::HashMap; +use std::{cmp::min, collections::HashSet, mem::replace}; #[derive(Debug)] pub enum CreateRoomError { diff -r 278533359a93 -r 5febd2bc5372 rust/hedgewars-server/src/handlers.rs --- a/rust/hedgewars-server/src/handlers.rs Mon Feb 17 16:38:24 2025 +0100 +++ b/rust/hedgewars-server/src/handlers.rs Sat Feb 22 19:39:31 2025 +0300 @@ -28,6 +28,7 @@ types::{GameCfg, TeamInfo}, }; +use crate::core::digest::Sha1Digest; use base64::encode; use log::*; use rand::{thread_rng, RngCore}; @@ -40,53 +41,6 @@ mod inroom; mod strings; -#[derive(PartialEq, Debug)] -pub struct Sha1Digest([u8; 20]); - -impl Sha1Digest { - pub fn new(digest: [u8; 20]) -> Self { - Self(digest) - } -} - -impl LowerHex for Sha1Digest { - fn fmt(&self, f: &mut Formatter) -> Result<(), std::fmt::Error> { - for byte in &self.0 { - write!(f, "{:02x}", byte)?; - } - Ok(()) - } -} - -impl PartialEq<&str> for Sha1Digest { - fn eq(&self, other: &&str) -> bool { - if other.len() != self.0.len() * 2 { - false - } else { - #[inline] - fn convert(c: u8) -> u8 { - if c > b'9' { - c.wrapping_sub(b'a').saturating_add(10) - } else { - c.wrapping_sub(b'0') - } - } - - other - .as_bytes() - .chunks_exact(2) - .zip(&self.0) - .all(|(chars, byte)| { - if let [hi, lo] = chars { - convert(*lo) == byte & 0x0f && convert(*hi) == (byte & 0xf0) >> 4 - } else { - unreachable!() - } - }) - } - } -} - pub struct ServerState { pub server: HwServer, pub anteroom: HwAnteroom, @@ -270,16 +224,14 @@ HwProtocolMessage::Ping => response.add(Pong.send_self()), HwProtocolMessage::Pong => (), _ => { - if state.anteroom.clients.contains(client_id) { - match inanteroom::handle(state, client_id, response, message) { + if state.anteroom.has_client(client_id) { + match inanteroom::handle(&mut state.anteroom, client_id, response, message) { LoginResult::Unchanged => (), - LoginResult::Complete => { - if let Some(client) = state.anteroom.remove_client(client_id) { - let is_checker = client.is_checker; - state.server.add_client(client_id, client); - if !is_checker { - common::get_lobby_join_data(&state.server, response); - } + LoginResult::Complete(client) => { + let is_checker = client.is_checker; + state.server.add_client(client_id, client); + if !is_checker { + common::get_lobby_join_data(&state.server, response); } } LoginResult::Exit => { @@ -292,12 +244,18 @@ HwProtocolMessage::Quit(Some(msg)) => { common::remove_client( &mut state.server, + &mut state.anteroom, response, "User quit: ".to_string() + &msg, ); } HwProtocolMessage::Quit(None) => { - common::remove_client(&mut state.server, response, "User quit".to_string()); + common::remove_client( + &mut state.server, + &mut state.anteroom, + response, + "User quit".to_string(), + ); } HwProtocolMessage::Info(nick) => { if let Some(client) = state.server.find_client(&nick) { @@ -394,7 +352,7 @@ .filter(|_| !is_local) .and_then(|a| state.anteroom.find_ip_ban(a)); if let Some(reason) = ban_reason { - response.add(HwServerMessage::Bye(reason).send_self()); + response.add(Bye(reason).send_self()); response.remove_client(client_id); false } else { @@ -405,17 +363,20 @@ .anteroom .add_client(client_id, encode(&salt), is_local); - response.add( - HwServerMessage::Connected(utils::SERVER_MESSAGE.to_owned(), utils::SERVER_VERSION) - .send_self(), - ); + response + .add(Connected(utils::SERVER_MESSAGE.to_owned(), utils::SERVER_VERSION).send_self()); true } } pub fn handle_client_loss(state: &mut ServerState, client_id: ClientId, response: &mut Response) { if state.anteroom.remove_client(client_id).is_none() { - common::remove_client(&mut state.server, response, "Connection reset".to_string()); + common::remove_client( + &mut state.server, + &mut state.anteroom, + response, + "Connection reset".to_string(), + ); } } @@ -431,7 +392,7 @@ response.add(Bye(REGISTRATION_REQUIRED.to_string()).send_self()); response.remove_client(client_id); } else if is_registered { - let client = &state.anteroom.clients[client_id]; + let client = state.anteroom.get_client(client_id); response.add(AskPassword(client.server_salt.clone()).send_self()); } else if let Some(client) = state.anteroom.remove_client(client_id) { state.server.add_client(client_id, client); @@ -508,7 +469,7 @@ #[cfg(test)] mod test { - use super::Sha1Digest; + use crate::core::digest::Sha1Digest; #[test] fn hash_cmp_test() { diff -r 278533359a93 -r 5febd2bc5372 rust/hedgewars-server/src/handlers/actions.rs --- a/rust/hedgewars-server/src/handlers/actions.rs Mon Feb 17 16:38:24 2025 +0100 +++ b/rust/hedgewars-server/src/handlers/actions.rs Sat Feb 22 19:39:31 2025 +0300 @@ -1,19 +1,5 @@ -use crate::{ - core::{ - client::HwClient, - room::HwRoom, - room::{GameInfo, RoomFlags}, - server::HwServer, - types::{ClientId, RoomId}, - }, - utils::to_engine_msg, -}; -use hedgewars_network_protocol::{ - messages::{server_chat, HwProtocolMessage, HwServerMessage, HwServerMessage::*}, - types::{GameCfg, VoteType}, -}; -use rand::{distributions::Uniform, thread_rng, Rng}; -use std::{io, io::Write, iter::once, mem::replace}; +use crate::core::types::{ClientId, RoomId}; +use hedgewars_network_protocol::messages::{HwServerMessage, HwServerMessage::*}; #[derive(Clone)] pub enum DestinationGroup { diff -r 278533359a93 -r 5febd2bc5372 rust/hedgewars-server/src/handlers/common.rs --- a/rust/hedgewars-server/src/handlers/common.rs Mon Feb 17 16:38:24 2025 +0100 +++ b/rust/hedgewars-server/src/handlers/common.rs Sat Feb 22 19:39:31 2025 +0300 @@ -2,6 +2,7 @@ actions::{Destination, DestinationGroup}, Response, }; +use crate::core::anteroom::HwAnteroom; use crate::core::server::HwRoomOrServer; use crate::handlers::actions::ToPendingMessage; use crate::{ @@ -354,10 +355,16 @@ } } -pub fn remove_client(server: &mut HwServer, response: &mut Response, msg: String) { +pub fn remove_client( + server: &mut HwServer, + anteroom: &mut HwAnteroom, + response: &mut Response, + msg: String, +) { let client_id = response.client_id(); let client = server.client(client_id); let nick = client.nick.clone(); + anteroom.forget_nick(&nick); match server.get_room_control(client_id) { HwRoomOrServer::Room(mut control) => { diff -r 278533359a93 -r 5febd2bc5372 rust/hedgewars-server/src/handlers/inanteroom.rs --- a/rust/hedgewars-server/src/handlers/inanteroom.rs Mon Feb 17 16:38:24 2025 +0100 +++ b/rust/hedgewars-server/src/handlers/inanteroom.rs Sat Feb 22 19:39:31 2025 +0300 @@ -20,42 +20,32 @@ pub enum LoginResult { Unchanged, - Complete, + Complete(HwAnteroomClient), Exit, } -fn completion_result<'a, I>( - mut other_clients: I, - client: &mut HwAnteroomClient, +fn get_completion_result( + anteroom: &mut HwAnteroom, + client_id: ClientId, response: &mut super::Response, -) -> LoginResult -where - I: Iterator, -{ - let has_nick_clash = other_clients.any(|c| c.nick == *client.nick.as_ref().unwrap()); - - if has_nick_clash { - client.nick = None; - response.add(Notice("NickAlreadyInUse".to_string()).send_self()); +) -> LoginResult { + #[cfg(feature = "official-server")] + { + let client = anteroom.get_client(client_id); + response.request_io(super::IoTask::CheckRegistered { + nick: client.nick.as_ref().unwrap().clone(), + }); LoginResult::Unchanged - } else { - #[cfg(feature = "official-server")] - { - response.request_io(super::IoTask::CheckRegistered { - nick: client.nick.as_ref().unwrap().clone(), - }); - LoginResult::Unchanged - } + } - #[cfg(not(feature = "official-server"))] - { - LoginResult::Complete - } + #[cfg(not(feature = "official-server"))] + { + LoginResult::Complete(anteroom.remove_client(client_id).unwrap()) } } pub fn handle( - server_state: &mut super::ServerState, + anteroom: &mut HwAnteroom, client_id: ClientId, response: &mut super::Response, message: HwProtocolMessage, @@ -66,8 +56,15 @@ response.add(Bye("User quit".to_string()).send_self()); LoginResult::Exit } - HwProtocolMessage::Nick(nick) => { - let client = &mut server_state.anteroom.clients[client_id]; + HwProtocolMessage::Nick(nick, token) => { + if anteroom.nick_taken(&nick) { + response.add(Notice("NickAlreadyInUse".to_string()).send_self()); + return LoginResult::Unchanged; + } + let reconnect = token + .map(|t| anteroom.get_nick_token(&nick) == Some(&t[..])) + .unwrap_or(false); + let client = anteroom.get_client_mut(client_id); if client.nick.is_some() { response.error(NICKNAME_PROVIDED); @@ -77,17 +74,23 @@ LoginResult::Exit } else { client.nick = Some(nick.clone()); + let protocol_number = client.protocol_number; + if reconnect { + client.is_registered = reconnect; + } else if let Some(token) = anteroom.register_nick_token(&nick) { + response.add(Token(token.to_string()).send_self()); + } response.add(Nick(nick).send_self()); - if client.protocol_number.is_some() { - completion_result(server_state.server.iter_clients(), client, response) + if protocol_number.is_some() { + get_completion_result(anteroom, client_id, response) } else { LoginResult::Unchanged } } } HwProtocolMessage::Proto(proto) => { - let client = &mut server_state.anteroom.clients[client_id]; + let client = anteroom.get_client_mut(client_id); if client.protocol_number.is_some() { response.error(PROTOCOL_PROVIDED); LoginResult::Unchanged @@ -99,7 +102,7 @@ response.add(Proto(proto).send_self()); if client.nick.is_some() { - completion_result(server_state.server.iter_clients(), client, response) + get_completion_result(anteroom, client_id, response) } else { LoginResult::Unchanged } @@ -107,7 +110,7 @@ } #[cfg(feature = "official-server")] HwProtocolMessage::Password(hash, salt) => { - let client = &server_state.anteroom.clients[client_id]; + let client = anteroom.get_client(client_id); if let (Some(nick), Some(protocol)) = (client.nick.as_ref(), client.protocol_number) { response.request_io(super::IoTask::GetAccount { @@ -123,7 +126,7 @@ } #[cfg(feature = "official-server")] HwProtocolMessage::Checker(protocol, nick, password) => { - let client = &mut server_state.anteroom.clients[client_id]; + let client = anteroom.get_client_mut(client_id); if protocol == 0 { response.error("Bad number."); LoginResult::Unchanged @@ -142,7 +145,8 @@ #[cfg(feature = "official-server")] { response.add(LogonPassed.send_self()); - LoginResult::Complete + anteroom.remember_nick(nick); + LoginResult::Complete(anteroom.remove_client(client_id).unwrap()) } } }