From ed83a77b3bc2ced64290cc6a8d6cf6273dc3be5e Mon Sep 17 00:00:00 2001 From: Denis Drakhnia Date: Sun, 8 Oct 2023 17:07:02 +0300 Subject: [PATCH] Add update client message --- config/main.toml | 7 ++++ src/client.rs | 2 + src/config.rs | 32 +++++++++++++-- src/filter.rs | 48 +++++++++++++++++++++- src/master_server.rs | 96 +++++++++++++++++++++++++++++++++----------- src/parser.rs | 7 ++++ 6 files changed, 165 insertions(+), 27 deletions(-) diff --git a/config/main.toml b/config/main.toml index 8f694d1..adda896 100644 --- a/config/main.toml +++ b/config/main.toml @@ -13,3 +13,10 @@ port = 27010 challenge = 300 # Time in seconds while server is valid server = 300 + +[client] +# If client version is less then show update message +version = "0.20" +update_title = "https://github.com/FWGS/xash3d-fwgs" +update_map = "Update please" +update_addr = "127.0.0.1:27010" diff --git a/src/client.rs b/src/client.rs index 49e7cbf..04e619c 100644 --- a/src/client.rs +++ b/src/client.rs @@ -41,6 +41,7 @@ pub enum Packet<'a> { ServerAdd(Option, ServerInfo<&'a str>), ServerRemove, QueryServers(Region, Filter<'a>), + ServerInfo, } impl<'a> Packet<'a> { @@ -70,6 +71,7 @@ impl<'a> Packet<'a> { } [b'b', b'\n'] => Ok(Self::ServerRemove), [b'q'] => Ok(Self::Challenge(None)), + [0xff, 0xff, 0xff, 0xff, b'i', b'n', b'f', b'o', b' ', _, _] => Ok(Self::ServerInfo), _ => Err(Error::InvalidPacket), } } diff --git a/src/config.rs b/src/config.rs index a0cc518..681cb22 100644 --- a/src/config.rs +++ b/src/config.rs @@ -3,13 +3,15 @@ use std::fs; use std::io; -use std::net::{IpAddr, Ipv4Addr}; +use std::net::{IpAddr, Ipv4Addr, SocketAddrV4}; use std::path::Path; use log::LevelFilter; use serde::{de::Error as _, Deserialize, Deserializer}; use thiserror::Error; +use crate::filter::Version; + pub const DEFAULT_CONFIG_PATH: &str = "config/main.toml"; pub const DEFAULT_MASTER_SERVER_IP: IpAddr = IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)); @@ -31,6 +33,8 @@ pub struct Config { pub log: LogConfig, #[serde(default)] pub server: ServerConfig, + #[serde(default)] + pub client: ClientConfig, } #[derive(Deserialize, Debug)] @@ -88,6 +92,20 @@ impl Default for TimeoutConfig { } } +#[derive(Deserialize, Default, Debug)] +#[serde(deny_unknown_fields)] +pub struct ClientConfig { + #[serde(default)] + #[serde(deserialize_with = "deserialize_version")] + pub version: Version, + #[serde(default)] + pub update_map: Box, + #[serde(default)] + pub update_title: Box, + #[serde(default)] + pub update_addr: Option, +} + fn default_log_level() -> LevelFilter { LevelFilter::Warn } @@ -109,8 +127,7 @@ where D: Deserializer<'de>, { let s = <&str>::deserialize(de)?; - parse_log_level(s) - .ok_or_else(|| D::Error::custom(format!("Invalid value for log option: \"{}\"", s))) + parse_log_level(s).ok_or_else(|| D::Error::custom(format!("Invalid log level: \"{}\"", s))) } pub fn parse_log_level(s: &str) -> Option { @@ -136,6 +153,15 @@ pub fn parse_log_level(s: &str) -> Option { Some(level_filter) } +fn deserialize_version<'de, D>(de: D) -> Result +where + D: Deserializer<'de>, +{ + let s = <&str>::deserialize(de)?; + s.parse() + .map_err(|_| D::Error::custom(format!("Invalid version: \"{}\"", s))) +} + pub fn load>(path: P) -> Result { let data = fs::read(path)?; Ok(toml::de::from_slice(&data)?) diff --git a/src/filter.rs b/src/filter.rs index 03c18d6..212de90 100644 --- a/src/filter.rs +++ b/src/filter.rs @@ -28,7 +28,10 @@ //! * Do not have bots //! * Is not protected by a password +use std::fmt; use std::net::SocketAddrV4; +use std::num::ParseIntError; +use std::str::FromStr; use bitflags::bitflags; use log::{debug, log_enabled, Level}; @@ -79,6 +82,49 @@ impl From<&ServerInfo> for FilterFlags { } } +#[derive(Copy, Clone, Default, PartialEq, Eq, PartialOrd, Ord)] +pub struct Version { + pub major: u8, + pub minor: u8, +} + +impl Version { + pub fn new(major: u8, minor: u8) -> Self { + Self { major, minor } + } +} + +impl fmt::Debug for Version { + fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { + write!(fmt, "{}.{}", self.major, self.minor) + } +} + +impl fmt::Display for Version { + fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { + write!(fmt, "{}.{}", self.major, self.minor) + } +} + +impl FromStr for Version { + type Err = ParseIntError; + + fn from_str(s: &str) -> Result { + let (major, minor) = s.split_once('.').unwrap_or((s, "0")); + Ok(Self::new(major.parse()?, minor.parse()?)) + } +} + +impl ParseValue<'_> for Version { + type Err = ParserError; + + fn parse(p: &mut Parser<'_>) -> Result { + let s = p.parse::<&str>()?; + let v = s.parse()?; + Ok(v) + } +} + #[derive(Clone, Debug, Default, PartialEq, Eq)] pub struct Filter<'a> { /// Servers running the specified modification (ex. cstrike) @@ -92,7 +138,7 @@ pub struct Filter<'a> { /// Return only servers on the specified IP address (port supported and optional) pub gameaddr: Option, /// Client version. - pub clver: Option<&'a str>, + pub clver: Option, pub flags: FilterFlags, pub flags_mask: FilterFlags, diff --git a/src/master_server.rs b/src/master_server.rs index e92554d..3f3c0da 100644 --- a/src/master_server.rs +++ b/src/master_server.rs @@ -14,7 +14,7 @@ use thiserror::Error; use crate::client::Packet; use crate::config::{self, Config}; -use crate::filter::Filter; +use crate::filter::{Filter, Version}; use crate::server::Server; use crate::server_info::Region; @@ -74,13 +74,19 @@ impl Deref for Entry { struct MasterServer { sock: UdpSocket, - start_time: Instant, challenges: HashMap>, servers: HashMap>, rng: Rng, + + start_time: Instant, cleanup_challenges: usize, cleanup_servers: usize, timeout: config::TimeoutConfig, + + clver: Version, + update_title: Box, + update_map: Box, + update_addr: SocketAddrV4, } impl MasterServer { @@ -88,6 +94,13 @@ impl MasterServer { let addr = SocketAddr::new(cfg.server.ip, cfg.server.port); info!("Listen address: {}", addr); let sock = UdpSocket::bind(addr).map_err(Error::BindSocket)?; + let update_addr = + cfg.client + .update_addr + .unwrap_or_else(|| match sock.local_addr().unwrap() { + SocketAddr::V4(addr) => addr, + _ => todo!(), + }); Ok(Self { sock, @@ -98,6 +111,10 @@ impl MasterServer { cleanup_challenges: 0, cleanup_servers: 0, timeout: cfg.server.timeout, + clver: cfg.client.version, + update_title: cfg.client.update_title, + update_map: cfg.client.update_map, + update_addr, }) } @@ -120,7 +137,14 @@ impl MasterServer { } fn handle_packet(&mut self, from: SocketAddrV4, s: &[u8]) -> Result<(), Error> { - let packet = Packet::decode(s)?; + let packet = match Packet::decode(s) { + Ok(p) => p, + Err(_) => { + trace!("{}: Failed to decode {:?}", from, s); + return Ok(()); + } + }; + trace!("{}: recv {:?}", from, packet); match packet { @@ -158,13 +182,48 @@ impl MasterServer { self.remove_outdated_servers(); } Packet::ServerRemove => { /* ignore */ } - Packet::QueryServers(region, filter) => match Filter::from_bytes(&filter) { - Ok(filter) => self.send_server_list(from, region, &filter)?, - _ => { - warn!("{}: Invalid filter: {:?}", from, filter); - return Ok(()); + Packet::QueryServers(region, filter) => { + let filter = match Filter::from_bytes(&filter) { + Ok(f) => f, + _ => { + warn!("{}: Invalid filter: {:?}", from, filter); + return Ok(()); + } + }; + + if filter.clver.map_or(true, |v| v < self.clver) { + let iter = std::iter::once(&self.update_addr); + self.send_server_list(from, iter)?; + } else { + let now = self.now(); + let iter = self + .servers + .iter() + .filter(|i| i.1.is_valid(now, self.timeout.server)) + .filter(|i| i.1.matches(*i.0, region, &filter)) + .map(|i| i.0); + self.send_server_list(from, iter)?; } - }, + } + Packet::ServerInfo => { + let mut buf = [0; MAX_PACKET_SIZE]; + let mut cur = Cursor::new(&mut buf[..]); + cur.write_all(b"\xff\xff\xff\xffinfo\n")?; + cur.write_all(b"\\p\\49")?; + cur.write_all(b"\\map\\")?; + cur.write_all(self.update_map.as_bytes())?; + cur.write_all(b"\\dm\\1")?; + cur.write_all(b"\\team\\0")?; + cur.write_all(b"\\coop\\0")?; + cur.write_all(b"\\numcl\\0")?; + cur.write_all(b"\\maxcl\\0")?; + cur.write_all(b"\\gamedir\\valve")?; + cur.write_all(b"\\password\\0")?; + cur.write_all(b"\\host\\")?; + cur.write_all(self.update_title.as_bytes())?; + let n = cur.position() as usize; + self.sock.send_to(&buf[..n], from)?; + } } Ok(()) @@ -240,20 +299,11 @@ impl MasterServer { Ok(()) } - fn send_server_list( - &self, - to: A, - region: Region, - filter: &Filter, - ) -> Result<(), io::Error> { - let now = self.now(); - let mut iter = self - .servers - .iter() - .filter(|i| i.1.is_valid(now, self.timeout.server)) - .filter(|i| i.1.matches(*i.0, region, filter)) - .map(|i| i.0); - + fn send_server_list<'a, A, I>(&self, to: A, mut iter: I) -> Result<(), io::Error> + where + A: ToSocketAddrs, + I: Iterator, + { let mut buf = [0; MAX_PACKET_SIZE]; let mut done = false; while !done { diff --git a/src/parser.rs b/src/parser.rs index 3eddb4f..0f86ded 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -1,6 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-only // SPDX-FileCopyrightText: 2023 Denis Drakhnia +use std::num::ParseIntError; use std::str; use thiserror::Error; @@ -19,6 +20,12 @@ pub enum Error { InvalidInteger, } +impl From for Error { + fn from(_: ParseIntError) -> Self { + Error::InvalidInteger + } +} + pub type Result = std::result::Result; pub struct Parser<'a> {