From caa7329a68bb03b78d67a5a59bb62d8276090bab Mon Sep 17 00:00:00 2001 From: CosminPerRam Date: Tue, 15 Nov 2022 21:07:15 +0200 Subject: [PATCH] Added socket timeout capability and reduced PACKET_SIZE to 1400 as specified from protocol --- CHANGELOG.md | 8 +++- examples/master_querant.rs | 6 +-- src/errors.rs | 6 +++ src/games/aliens.rs | 2 +- src/games/asrd.rs | 2 +- src/games/cscz.rs | 2 +- src/games/csgo.rs | 4 +- src/games/css.rs | 2 +- src/games/dod.rs | 2 +- src/games/dods.rs | 2 +- src/games/gm.rs | 2 +- src/games/hl2dm.rs | 2 +- src/games/ins.rs | 2 +- src/games/insmic.rs | 2 +- src/games/inss.rs | 2 +- src/games/l4d.rs | 2 +- src/games/l4d2.rs | 2 +- src/games/tf2.rs | 2 +- src/games/ts.rs | 2 +- src/protocols/valve/protocol.rs | 53 ++++++++++++++------------ src/protocols/valve/types.rs | 67 ++++++++++++++++++++++++++++++++- 21 files changed, 125 insertions(+), 49 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3e65158..7c1750d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,16 +2,20 @@ Who knows what the future holds... # 0.0.5 - ??/??/2022 +Added `SocketBind` error, regarding failing to bind a socket. +Socket custom timeout capability (with an error if provided durations are zero). +Because of this, a parameter similar to GatherSettings has been added on the Valve Protocol Query. Support for GoldSrc split packets and obsolete A2S_INFO response. Changed the Valve Protocol app parameter to represent the engine responses. It is now an enum of: -- `Source(Option)` - A Source response with optionally, the id (if the id is present and the response id is not the same, the query fails) +- `Source(Option)` - A Source response with optionally, the id (if the id is present and the response id is not the same, the query fails), if it isn't provided, find it. - `GoldSrc(bool)` - A GoldSrc response with the option to enforce the obsolete A2S_INFO response. +Fixed Source multi-packet response crash due to when a certain app with a certain protocol doesn't have the Size field. +Reduced Valve Protocol `PACKET_SIZE` to be as specified from 2048 to 1400. [Counter-Strike: Condition Zero](https://store.steampowered.com/app/80/CounterStrike_Condition_Zero/) implementation. [Day of Defeat](https://store.steampowered.com/app/30/Day_of_Defeat/) implementation. Games besides CSGO and TS now have the same response structure. -Fixed Source multipacket response crash due to when a certain app with a certain protocol doesnt have the Size field. # 0.0.4 - 23/10/2022 Queries now support DNS resolve. diff --git a/examples/master_querant.rs b/examples/master_querant.rs index 4270f0c..015374d 100644 --- a/examples/master_querant.rs +++ b/examples/master_querant.rs @@ -41,9 +41,9 @@ fn main() -> GDResult<()> { "ts" => println!("{:?}", ts::query(ip, port)?), "cscz" => println!("{:?}", cscz::query(ip, port)?), "dod" => println!("{:?}", dod::query(ip, port)?), - "_src" => println!("{:?}", valve::query(ip, 27015, App::Source(None), None)?), - "_gld" => println!("{:?}", valve::query(ip, 27015, App::GoldSrc(false), None)?), - "_gld_f" => println!("{:?}", valve::query(ip, 27015, App::GoldSrc(true), None)?), + "_src" => println!("{:?}", valve::query(ip, 27015, App::Source(None), None, None)?), + "_gld" => println!("{:?}", valve::query(ip, 27015, App::GoldSrc(false), None, None)?), + "_gld_f" => println!("{:?}", valve::query(ip, 27015, App::GoldSrc(true), None, None)?), _ => panic!("Undefined game: {}", args[1]) }; diff --git a/src/errors.rs b/src/errors.rs index 68e1528..584337e 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -28,6 +28,10 @@ pub enum GDError { BadGame(String), /// Problems occurred while dns resolving. DnsResolve(String), + /// Couldn't bind a socket. + SocketBind(String), + /// Invalid input. + InvalidInput(String), } impl fmt::Display for GDError { @@ -42,6 +46,8 @@ impl fmt::Display for GDError { GDError::UnknownEnumCast => write!(f, "Unknown enum cast encountered."), GDError::BadGame(details) => write!(f, "Queried another game that the supposed one: {details}"), GDError::DnsResolve(details) => write!(f, "DNS Resolve: {details}"), + GDError::SocketBind(details) => write!(f, "Socket bind: {details}"), + GDError::InvalidInput(details) => write!(f, "Invalid input: {details}"), } } } diff --git a/src/games/aliens.rs b/src/games/aliens.rs index e5ace35..3e8dc43 100644 --- a/src/games/aliens.rs +++ b/src/games/aliens.rs @@ -6,7 +6,7 @@ pub fn query(address: &str, port: Option) -> GDResult { let valve_response = valve::query(address, match port { None => 27015, Some(port) => port - }, SteamID::ALIENS.app(), None)?; + }, SteamID::ALIENS.as_app(), None, None)?; Ok(game::Response::new_from_valve_response(valve_response)) } diff --git a/src/games/asrd.rs b/src/games/asrd.rs index 32432c3..4852233 100644 --- a/src/games/asrd.rs +++ b/src/games/asrd.rs @@ -6,7 +6,7 @@ pub fn query(address: &str, port: Option) -> GDResult { let valve_response = valve::query(address, match port { None => 27015, Some(port) => port - }, SteamID::ASRD.app(), None)?; + }, SteamID::ASRD.as_app(), None, None)?; Ok(game::Response::new_from_valve_response(valve_response)) } diff --git a/src/games/cscz.rs b/src/games/cscz.rs index ac11aa9..56106dc 100644 --- a/src/games/cscz.rs +++ b/src/games/cscz.rs @@ -6,7 +6,7 @@ pub fn query(address: &str, port: Option) -> GDResult { let valve_response = valve::query(address, match port { None => 27015, Some(port) => port - }, SteamID::CSCZ.app(), None)?; + }, SteamID::CSCZ.as_app(), None, None)?; Ok(game::Response::new_from_valve_response(valve_response)) } diff --git a/src/games/csgo.rs b/src/games/csgo.rs index 3f4785e..2ad965e 100644 --- a/src/games/csgo.rs +++ b/src/games/csgo.rs @@ -54,10 +54,10 @@ pub fn query(address: &str, port: Option) -> GDResult { let valve_response = valve::query(address, match port { None => 27015, Some(port) => port - }, SteamID::CSGO.app(), Some(GatheringSettings { + }, SteamID::CSGO.as_app(), Some(GatheringSettings { players: true, rules: false // cause csgo doesnt reply with rules anymore - }))?; + }), None)?; Ok(Response::new_from_valve_response(valve_response)) } diff --git a/src/games/css.rs b/src/games/css.rs index cc45ebe..383a3f6 100644 --- a/src/games/css.rs +++ b/src/games/css.rs @@ -6,7 +6,7 @@ pub fn query(address: &str, port: Option) -> GDResult { let valve_response = valve::query(address, match port { None => 27015, Some(port) => port - }, SteamID::CSS.app(), None)?; + }, SteamID::CSS.as_app(), None, None)?; Ok(game::Response::new_from_valve_response(valve_response)) } diff --git a/src/games/dod.rs b/src/games/dod.rs index 136541c..928f94f 100644 --- a/src/games/dod.rs +++ b/src/games/dod.rs @@ -6,7 +6,7 @@ pub fn query(address: &str, port: Option) -> GDResult { let valve_response = valve::query(address, match port { None => 27015, Some(port) => port - }, SteamID::DOD.app(), None)?; + }, SteamID::DOD.as_app(), None, None)?; Ok(game::Response::new_from_valve_response(valve_response)) } diff --git a/src/games/dods.rs b/src/games/dods.rs index fa4c772..0e3cdd1 100644 --- a/src/games/dods.rs +++ b/src/games/dods.rs @@ -6,7 +6,7 @@ pub fn query(address: &str, port: Option) -> GDResult { let valve_response = valve::query(address, match port { None => 27015, Some(port) => port - }, SteamID::DODS.app(), None)?; + }, SteamID::DODS.as_app(), None, None)?; Ok(game::Response::new_from_valve_response(valve_response)) } diff --git a/src/games/gm.rs b/src/games/gm.rs index 6d9a0ee..7040c28 100644 --- a/src/games/gm.rs +++ b/src/games/gm.rs @@ -6,7 +6,7 @@ pub fn query(address: &str, port: Option) -> GDResult { let valve_response = valve::query(address, match port { None => 27015, Some(port) => port - }, SteamID::GM.app(), None)?; + }, SteamID::GM.as_app(), None, None)?; Ok(game::Response::new_from_valve_response(valve_response)) } diff --git a/src/games/hl2dm.rs b/src/games/hl2dm.rs index 42e7fae..c45741e 100644 --- a/src/games/hl2dm.rs +++ b/src/games/hl2dm.rs @@ -6,7 +6,7 @@ pub fn query(address: &str, port: Option) -> GDResult { let valve_response = valve::query(address, match port { None => 27015, Some(port) => port - }, SteamID::HL2DM.app(), None)?; + }, SteamID::HL2DM.as_app(), None, None)?; Ok(game::Response::new_from_valve_response(valve_response)) } diff --git a/src/games/ins.rs b/src/games/ins.rs index f1091ec..82341b3 100644 --- a/src/games/ins.rs +++ b/src/games/ins.rs @@ -6,7 +6,7 @@ pub fn query(address: &str, port: Option) -> GDResult { let valve_response = valve::query(address, match port { None => 27015, Some(port) => port - }, SteamID::INS.app(), None)?; + }, SteamID::INS.as_app(), None, None)?; Ok(game::Response::new_from_valve_response(valve_response)) } diff --git a/src/games/insmic.rs b/src/games/insmic.rs index 6e1a869..cf31b69 100644 --- a/src/games/insmic.rs +++ b/src/games/insmic.rs @@ -6,7 +6,7 @@ pub fn query(address: &str, port: Option) -> GDResult { let valve_response = valve::query(address, match port { None => 27015, Some(port) => port - }, SteamID::INSMIC.app(), None)?; + }, SteamID::INSMIC.as_app(), None, None)?; Ok(game::Response::new_from_valve_response(valve_response)) } diff --git a/src/games/inss.rs b/src/games/inss.rs index 47ae02d..d57c036 100644 --- a/src/games/inss.rs +++ b/src/games/inss.rs @@ -6,7 +6,7 @@ pub fn query(address: &str, port: Option) -> GDResult { let valve_response = valve::query(address, match port { None => 27131, Some(port) => port - }, SteamID::INSS.app(), None)?; + }, SteamID::INSS.as_app(), None, None)?; Ok(game::Response::new_from_valve_response(valve_response)) } diff --git a/src/games/l4d.rs b/src/games/l4d.rs index 3054aaa..58a861c 100644 --- a/src/games/l4d.rs +++ b/src/games/l4d.rs @@ -6,7 +6,7 @@ pub fn query(address: &str, port: Option) -> GDResult { let valve_response = valve::query(address, match port { None => 27015, Some(port) => port - }, SteamID::L4D.app(), None)?; + }, SteamID::L4D.as_app(), None, None)?; Ok(game::Response::new_from_valve_response(valve_response)) } diff --git a/src/games/l4d2.rs b/src/games/l4d2.rs index 32074f4..23dded8 100644 --- a/src/games/l4d2.rs +++ b/src/games/l4d2.rs @@ -6,7 +6,7 @@ pub fn query(address: &str, port: Option) -> GDResult { let valve_response = valve::query(address, match port { None => 27015, Some(port) => port - }, SteamID::L4D2.app(), None)?; + }, SteamID::L4D2.as_app(), None, None)?; Ok(game::Response::new_from_valve_response(valve_response)) } diff --git a/src/games/tf2.rs b/src/games/tf2.rs index 256eb4b..ba00201 100644 --- a/src/games/tf2.rs +++ b/src/games/tf2.rs @@ -6,7 +6,7 @@ pub fn query(address: &str, port: Option) -> GDResult { let valve_response = valve::query(address, match port { None => 27015, Some(port) => port - }, SteamID::TF2.app(), None)?; + }, SteamID::TF2.as_app(), None, None)?; Ok(game::Response::new_from_valve_response(valve_response)) } diff --git a/src/games/ts.rs b/src/games/ts.rs index 80b0f05..a59c4ae 100644 --- a/src/games/ts.rs +++ b/src/games/ts.rs @@ -84,7 +84,7 @@ pub fn query(address: &str, port: Option) -> GDResult { let valve_response = valve::query(address, match port { None => 27015, Some(port) => port - }, SteamID::TS.app(), None)?; + }, SteamID::TS.as_app(), None, None)?; Ok(Response::new_from_valve_response(valve_response)) } diff --git a/src/protocols/valve/protocol.rs b/src/protocols/valve/protocol.rs index 7c8c2f5..8aeceab 100644 --- a/src/protocols/valve/protocol.rs +++ b/src/protocols/valve/protocol.rs @@ -1,7 +1,7 @@ use std::net::UdpSocket; use bzip2_rs::decoder::Decoder; use crate::{GDError, GDResult}; -use crate::protocols::valve::{App, ModData, SteamID}; +use crate::protocols::valve::{App, ModData, SteamID, TimeoutSettings}; use crate::protocols::valve::types::{Environment, ExtraData, GatheringSettings, Request, Response, Server, ServerInfo, ServerPlayer, ServerRule, TheShip}; use crate::utils::{buffer, complete_address, u8_lower_upper}; @@ -87,7 +87,7 @@ impl SplitPacket { App::Source(_) => { let total = buffer::get_u8(&buf, &mut pos)?; let number = buffer::get_u8(&buf, &mut pos)?; - let size = match protocol == 7 && (*app == SteamID::CSS.app()) { //certain apps with protocol = 7 doesnt have this field + let size = match protocol == 7 && (*app == SteamID::CSS.as_app()) { //certain apps with protocol = 7 doesnt have this field false => buffer::get_u16_le(&buf, &mut pos)?, true => 1248 }; @@ -143,12 +143,17 @@ struct ValveProtocol { complete_address: String } -static DEFAULT_PACKET_SIZE: usize = 1400; +static PACKET_SIZE: usize = 1400; impl ValveProtocol { - fn new(address: &str, port: u16) -> GDResult { + fn new(address: &str, port: u16, timeout_settings: TimeoutSettings) -> GDResult { + let socket = UdpSocket::bind("0.0.0.0:0").map_err(|e| GDError::SocketBind(e.to_string()))?; + + socket.set_read_timeout(timeout_settings.get_read()).unwrap(); //unwrapping because TimeoutSettings::new + socket.set_write_timeout(timeout_settings.get_write()).unwrap();//checks if these are 0 and throws an error + Ok(Self { - socket: UdpSocket::bind("0.0.0.0:0").unwrap(), + socket, complete_address: complete_address(address, port)? }) } @@ -193,7 +198,7 @@ impl ValveProtocol { let request_initial_packet = Packet::initial(kind.clone()).to_bytes(); self.send(&request_initial_packet)?; - let packet = self.receive(app, protocol, DEFAULT_PACKET_SIZE)?; + let packet = self.receive(app, protocol, PACKET_SIZE)?; if packet.kind != 0x41 { //'A' return Ok(packet.payload.clone()); @@ -203,7 +208,7 @@ impl ValveProtocol { let challenge_packet = Packet::challenge(kind.clone(), challenge).to_bytes(); self.send(&challenge_packet)?; - Ok(self.receive(app, protocol, DEFAULT_PACKET_SIZE)?.payload) + Ok(self.receive(app, protocol, PACKET_SIZE)?.payload) } fn get_goldsrc_server_info(buf: &[u8]) -> GDResult { @@ -301,7 +306,7 @@ impl ValveProtocol { }; let has_password = buffer::get_u8(&buf, &mut pos)? == 1; let vac_secured = buffer::get_u8(&buf, &mut pos)? == 1; - let the_ship = match *app == SteamID::TS.app() { + let the_ship = match *app == SteamID::TS.as_app() { false => None, true => Some(TheShip { mode: buffer::get_u8(&buf, &mut pos)?, @@ -381,11 +386,11 @@ impl ValveProtocol { name: buffer::get_string(&buf, &mut pos)?, score: buffer::get_u32_le(&buf, &mut pos)?, duration: buffer::get_f32_le(&buf, &mut pos)?, - deaths: match *app == SteamID::TS.app() { + deaths: match *app == SteamID::TS.as_app() { false => None, true => Some(buffer::get_u32_le(&buf, &mut pos)?) }, - money: match *app == SteamID::TS.app() { + money: match *app == SteamID::TS.as_app() { false => None, true => Some(buffer::get_u32_le(&buf, &mut pos)?) } @@ -397,7 +402,7 @@ impl ValveProtocol { /// Get the server rules's. fn get_server_rules(&self, app: &App, protocol: u8) -> GDResult>> { - if *app == SteamID::CSGO.app() { //cause csgo wont respond to this since feb 21 2014 update + if *app == SteamID::CSGO.as_app() { //cause csgo wont respond to this since feb 21 2014 update return Ok(None); } @@ -418,10 +423,16 @@ impl ValveProtocol { } } -/// Query a server by providing the address, the port, the app and the gather settings, the settings -/// being *None* means to also get the players and the rules. -pub fn query(address: &str, port: u16, app: App, gather_settings: Option) -> GDResult { - let client = ValveProtocol::new(address, port)?; +/// Query a server by providing the address, the port, the app, gather and timeout settings. +/// Providing None to the settings results in using the default values for them (GatherSettings::[default](GatheringSettings::default), TimeoutSettings::[default](TimeoutSettings::default)). +pub fn query(address: &str, port: u16, app: App, gather_settings: Option, timeout_settings: Option) -> GDResult { + let response_gather_settings = gather_settings.unwrap_or(GatheringSettings::default()); + let response_timeout_settings = timeout_settings.unwrap_or(TimeoutSettings::default()); + get_response(address, port, app, response_gather_settings, response_timeout_settings) +} + +fn get_response(address: &str, port: u16, app: App, gather_settings: GatheringSettings, timeout_settings: TimeoutSettings) -> GDResult { + let client = ValveProtocol::new(address, port, timeout_settings)?; let info = client.get_server_info(&app)?; let protocol = info.protocol; @@ -434,21 +445,13 @@ pub fn query(address: &str, port: u16, app: App, gather_settings: Option (true, true), - true => { - let settings = gather_settings.unwrap(); - (settings.players, settings.rules) - } - }; - Ok(Response { info, - players: match gather_players { + players: match gather_settings.players { false => None, true => Some(client.get_server_players(&app, protocol)?) }, - rules: match gather_rules { + rules: match gather_settings.rules { false => None, true => client.get_server_rules(&app, protocol)? } diff --git a/src/protocols/valve/types.rs b/src/protocols/valve/types.rs index 6d612d5..a3819d2 100644 --- a/src/protocols/valve/types.rs +++ b/src/protocols/valve/types.rs @@ -1,3 +1,5 @@ +use std::time::Duration; +use crate::{GDError, GDResult}; /// The type of the server. #[derive(Debug)] @@ -111,6 +113,7 @@ pub struct ExtraData { pub game_id: Option } +/// Data related to GoldSrc Mod response. #[derive(Debug)] pub struct ModData { pub link: String, @@ -141,6 +144,7 @@ pub enum Request { } /// Supported steam apps id's +#[repr(u32)] #[derive(PartialEq, Clone)] pub enum SteamID { /// Day of Defeat @@ -178,10 +182,11 @@ pub enum SteamID { } impl SteamID { - pub fn app(self) -> App { + /// Get ID as App (the engine is specified). + pub fn as_app(&self) -> App { match self { SteamID::CSCZ | SteamID::DOD => App::GoldSrc(false), - x => App::Source(Some(x as u32)) + x => App::Source(Some(x.clone() as u32)) } } } @@ -204,6 +209,64 @@ pub struct GatheringSettings { pub rules: bool } +impl Default for GatheringSettings { + /// Default values are true for both the players and the rules. + fn default() -> Self { + Self { + players: true, + rules: true + } + } +} + +/// Timeout settings for socket operations +pub struct TimeoutSettings { + read: Option, + write: Option +} + +impl TimeoutSettings { + /// Construct new settings, passing None will block indefinitely. Passing zero Duration throws GDError::[InvalidInput](GDError::InvalidInput). + pub fn new(read: Option, write: Option) -> GDResult { + if let Some(read_duration) = read { + if read_duration == Duration::new(0, 0) { + return Err(GDError::InvalidInput("Can't pass duration 0 to timeout settings".to_owned())) + } + } + + if let Some(write_duration) = write { + if write_duration == Duration::new(0, 0) { + return Err(GDError::InvalidInput("Can't pass duration 0 to timeout settings".to_owned())) + } + } + + Ok(Self { + read, + write + }) + } + + /// Get the read timeout. + pub fn get_read(&self) -> Option { + self.read + } + + /// Get the write timeout. + pub fn get_write(&self) -> Option { + self.write + } +} + +impl Default for TimeoutSettings { + /// Default values are 4 seconds for both read and write. + fn default() -> Self { + Self { + read: Some(Duration::from_secs(4)), + write: Some(Duration::from_secs(4)) + } + } +} + /// Generic response types that are used by many games, they are the protocol ones, but without the /// unnecessary bits (example: the **The Ship**-only fields) pub mod game {