--- 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<String>),
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<String>),
@@ -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],
--- 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)
}
--- 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!(
--- 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<Ascii>),
10 => Proto(u16),
11 => Password(Ascii, Ascii),
12 => Checker(u16, Ascii, Ascii),
@@ -264,7 +264,7 @@
pub fn gen_server_msg() -> BoxedStrategy<HwServerMessage> 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<Ascii>)
+ 38 => Replay(Vec<Ascii>),
+ 39 => Token(Ascii)
)
});
res.boxed()
--- 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;
--- 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<HwAnteroomClient>,
+ clients: IndexSlab<HwAnteroomClient>,
bans: BanCollection,
+ taken_nicks: HashSet<String>,
+ reconnection_tokens: HashMap<String, String>,
}
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<HwAnteroomClient> {
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])
+ }
+ }
}
--- 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)
+ }
}
--- /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!()
+ }
+ })
+ }
+ }
+}
--- 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 {
--- 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() {
--- 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 {
--- 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) => {
--- 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<Item = &'a HwClient>,
-{
- 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())
}
}
}