diff --git a/CHANGELOG.md b/CHANGELOG.md index 433a33a..d04b2f3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ Who knows what the future holds... Protocols: - Valve: 1. Added standard and serde derives to `GatheringSettings`. +- Quake 1, 2 and 3 support. ### Breaking: - Every function that used `&str` for the address has been changed to `&IpAddr` (thanks [@Douile](https://github.com/Douile) for the re-re-write). diff --git a/PROTOCOLS.md b/PROTOCOLS.md index 39b7de1..4a67279 100644 --- a/PROTOCOLS.md +++ b/PROTOCOLS.md @@ -1,11 +1,12 @@ A protocol is defined as proprietary if it is being used only for a single scope (or series, like Minecraft). # Supported protocols: -| Name | For | Proprietary? | Documentation reference | Notes | -|----------------|-------|--------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| Valve Protocol | Games | No | [Server Queries](https://developer.valvesoftware.com/wiki/Server_queries) | In some cases, the players details query might contain some 0-length named players. Multi-packet decompression not tested. | -| Minecraft | Games | Yes | Java: [List Server Protocol](https://wiki.vg/Server_List_Ping)
Bedrock: [Node-GameDig Source](https://github.com/gamedig/node-gamedig/blob/master/protocols/minecraftbedrock.js) | | -| GameSpy | Games | No | One: [Node-GameDig Source](https://github.com/gamedig/node-gamedig/blob/master/protocols/gamespy1.js) Three: [Node-GameDig Source](https://github.com/gamedig/node-gamedig/blob/master/protocols/gamespy3.js) | These protocols are not really standardized, gamedig tries to get the most common fields amongst its supported games, if there are parsing problems, use the `query_vars` function. | +| Name | For | Proprietary? | Documentation reference | Notes | +|----------------|-------|--------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| Valve Protocol | Games | No | [Server Queries](https://developer.valvesoftware.com/wiki/Server_queries) | In some cases, the players details query might contain some 0-length named players. Multi-packet decompression not tested. | +| Minecraft | Games | Yes | Java: [List Server Protocol](https://wiki.vg/Server_List_Ping)
Bedrock: [Node-GameDig Source](https://github.com/gamedig/node-gamedig/blob/master/protocols/minecraftbedrock.js) | | +| GameSpy | Games | No | One: [Node-GameDig Source](https://github.com/gamedig/node-gamedig/blob/master/protocols/gamespy1.js) Three: [Node-GameDig Source](https://github.com/gamedig/node-gamedig/blob/master/protocols/gamespy3.js) | These protocols are not really standardized, gamedig tries to get the most common fields amongst its supported games, if there are parsing problems, use the `query_vars` function. | +| Quake | Games | No | One: [Node-GameDig Source](https://github.com/gamedig/node-gamedig/blob/master/protocols/quake1.js) Two: [Node-GameDig Source](https://github.com/gamedig/node-gamedig/blob/master/protocols/quake2.js) Three: [Node-GameDig Source](https://github.com/gamedig/node-gamedig/blob/master/protocols/quake3.js) | | ## Planned to add support: _ diff --git a/examples/master_querant.rs b/examples/master_querant.rs index f15f0b3..3b7488c 100644 --- a/examples/master_querant.rs +++ b/examples/master_querant.rs @@ -1,4 +1,4 @@ -use gamedig::protocols::gamespy; +use gamedig::protocols::{gamespy, quake}; use gamedig::protocols::minecraft::LegacyGroup; use gamedig::protocols::valve; use gamedig::protocols::valve::Engine; @@ -123,6 +123,9 @@ fn main() -> GDResult<()> { "_gamespy3_vars" => println!("{:#?}", gamespy::three::query_vars(ip, port.unwrap(), None)), "ffow" => println!("{:#?}", ffow::query(ip, port)), "cw" => println!("{:#?}", cw::query(ip, port)), + "_quake1" => println!("{:#?}", quake::one::query(ip, port.unwrap(), None)), + "_quake2" => println!("{:#?}", quake::two::query(ip, port.unwrap(), None)), + "_quake3" => println!("{:#?}", quake::three::query(ip, port.unwrap(), None)), _ => panic!("Undefined game: {}", args[1]), }; diff --git a/src/bufferer.rs b/src/bufferer.rs index 9daf84e..74e05c7 100644 --- a/src/bufferer.rs +++ b/src/bufferer.rs @@ -93,13 +93,13 @@ impl Bufferer { Ok(value) } - pub fn get_string_utf8(&mut self) -> GDResult { + fn get_string_utf8_until(&mut self, until: u8) -> GDResult { let sub_buf = self.remaining_data(); if sub_buf.is_empty() { return Err(PacketUnderflow); } - let first_null_position = sub_buf.iter().position(|&x| x == 0).ok_or(PacketBad)?; + let first_null_position = sub_buf.iter().position(|&x| x == until).ok_or(PacketBad)?; let value = std::str::from_utf8(&sub_buf[.. first_null_position]) .map_err(|_| PacketBad)? .to_string(); @@ -108,6 +108,14 @@ impl Bufferer { Ok(value) } + pub fn get_string_utf8(&mut self) -> GDResult { + self.get_string_utf8_until(0) + } + + pub fn get_string_utf8_newline(&mut self) -> GDResult { + self.get_string_utf8_until(10) + } + pub fn get_string_utf8_optional(&mut self) -> GDResult { match self.get_string_utf8() { Ok(data) => Ok(data), @@ -168,6 +176,8 @@ impl Bufferer { pub fn remaining_length(&self) -> usize { self.data_length() - self.position } + pub fn is_remaining_empty(&self) -> bool { self.remaining_length() == 0 } + pub fn as_endianess(&self, endianess: Endianess) -> Self { Bufferer { data: self.data.clone(), diff --git a/src/protocols/mod.rs b/src/protocols/mod.rs index 48a0e7e..97fc5bd 100644 --- a/src/protocols/mod.rs +++ b/src/protocols/mod.rs @@ -12,3 +12,5 @@ pub mod minecraft; pub mod types; /// Reference: [Server Query](https://developer.valvesoftware.com/wiki/Server_queries) pub mod valve; +/// Reference: [node-GameDig](https://github.com/gamedig/node-gamedig/blob/master/protocols/quake1.js) +pub mod quake; diff --git a/src/protocols/quake/client.rs b/src/protocols/quake/client.rs new file mode 100644 index 0000000..4b90a42 --- /dev/null +++ b/src/protocols/quake/client.rs @@ -0,0 +1,115 @@ +use std::collections::HashMap; +use std::net::IpAddr; +use std::slice::Iter; +use crate::bufferer::{Bufferer, Endianess}; +use crate::{GDError, GDResult}; +use crate::protocols::quake::types::Response; +use crate::protocols::types::TimeoutSettings; +use crate::socket::{Socket, UdpSocket}; + +pub(crate) trait QuakeClient { + type Player; + + fn get_send_header<'a>() -> &'a str; + fn get_response_header<'a>() -> &'a[u8]; + fn parse_player_string(data: Iter<&str>) -> GDResult; +} + +fn get_data(address: &IpAddr, port: u16, timeout_settings: Option) -> GDResult { + let mut socket = UdpSocket::new(address, port)?; + socket.apply_timeout(timeout_settings)?; + + socket.send(&[&[0xFF, 0xFF, 0xFF, 0xFF], Client::get_send_header().as_bytes(), &[0x00]].concat())?; + + let data = socket.receive(None)?; + let mut bufferer = Bufferer::new_with_data(Endianess::Little, &data); + + if bufferer.get_u32()? != 4294967295 { + return Err(GDError::PacketBad); + } + + let response_header = Client::get_response_header(); + if !bufferer.remaining_data().starts_with(response_header) { + Err(GDError::PacketBad)? + } + + bufferer.move_position_ahead(response_header.len()); + + Ok(bufferer) +} + +fn get_server_values(bufferer: &mut Bufferer) -> GDResult> { + let data = bufferer.get_string_utf8_newline()?; + let mut data_split = data.split('\\').collect::>(); + if let Some(first) = data_split.first() { + if first == &"" { + data_split.remove(0); + } + } + + let values = data_split.chunks(2); + + let mut vars: HashMap = HashMap::new(); + for data in values { + let key = data.first(); + let value = data.get(1); + + if let Some(k) = key { + if let Some(v) = value { + vars.insert(k.to_string(), v.to_string()); + } + } + } + + Ok(vars) +} + +fn get_players(bufferer: &mut Bufferer) -> GDResult> { + let mut players: Vec = Vec::new(); + + while !bufferer.is_remaining_empty() { + let data = bufferer.get_string_utf8_newline()?; + let data_split = data.split(' ').collect::>(); + let data_iter = data_split.iter(); + + players.push(Client::parse_player_string(data_iter)?) + } + + Ok(players) +} + +pub(crate) fn client_query(address: &IpAddr, port: u16, timeout_settings: Option) -> GDResult> { + let mut bufferer = get_data::(address, port, timeout_settings)?; + + let mut server_vars = get_server_values(&mut bufferer)?; + let players = get_players::(&mut bufferer)?; + + Ok(Response { + name: server_vars.remove("hostname") + .or(server_vars.remove("sv_hostname")) + .ok_or(GDError::PacketBad)?, + map: server_vars.remove("mapname") + .ok_or(GDError::PacketBad)?, + players_online: players.len() as u8, + players_maximum: server_vars.remove("maxclients") + .or(server_vars.remove("sv_maxclients")) + .ok_or(GDError::PacketBad)? + .parse() + .map_err(|_| GDError::TypeParse)?, + has_password: server_vars.remove("needpass") + .or(server_vars.remove("g_needpass")) + .ok_or(GDError::PacketBad)? == "1", + players, + frag_limit: server_vars.remove("fraglimit") + .ok_or(GDError::PacketBad)? + .parse() + .map_err(|_| GDError::TypeParse)?, + time_limit: server_vars.remove("timelimit") + .ok_or(GDError::PacketBad)? + .parse() + .map_err(|_| GDError::TypeParse)?, + version: server_vars.remove("version") + .ok_or(GDError::PacketBad)?, + unused_entries: server_vars, + }) +} diff --git a/src/protocols/quake/mod.rs b/src/protocols/quake/mod.rs new file mode 100644 index 0000000..da11432 --- /dev/null +++ b/src/protocols/quake/mod.rs @@ -0,0 +1,10 @@ + +pub mod one; +pub mod two; +pub mod three; + +/// All types used by the implementation. +pub mod types; +pub use types::*; + +mod client; diff --git a/src/protocols/quake/one.rs b/src/protocols/quake/one.rs new file mode 100644 index 0000000..bff3242 --- /dev/null +++ b/src/protocols/quake/one.rs @@ -0,0 +1,77 @@ +use std::net::IpAddr; +use std::slice::Iter; +use crate::{GDError, GDResult}; +use crate::protocols::quake::Response; +use crate::protocols::quake::client::{QuakeClient, client_query}; +use crate::protocols::types::TimeoutSettings; +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +/// Quake 1 player data. +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub struct Player { + /// Player's server id. + pub id: u8, + pub score: u8, + pub time: u8, + pub ping: u8, + pub name: String, + pub skin: String, + pub color_primary: u8, + pub color_secondary: u8 +} + +pub(crate) struct QuakeOne; +impl QuakeClient for QuakeOne { + type Player = Player; + + fn get_send_header<'a>() -> &'a str { + "status" + } + + fn get_response_header<'a>() -> &'a [u8] { + &[0x6E] + } + + fn parse_player_string(mut data: Iter<&str>) -> GDResult { + Ok(Player { + id: match data.next() { + None => Err(GDError::PacketBad)?, + Some(v) => v.parse().map_err(|_| GDError::PacketBad)? + }, + score: match data.next() { + None => Err(GDError::PacketBad)?, + Some(v) => v.parse().map_err(|_| GDError::PacketBad)? + }, + time: match data.next() { + None => Err(GDError::PacketBad)?, + Some(v) => v.parse().map_err(|_| GDError::PacketBad)? + }, + ping: match data.next() { + None => Err(GDError::PacketBad)?, + Some(v) => v.parse().map_err(|_| GDError::PacketBad)? + }, + name: match data.next() { + None => Err(GDError::PacketBad)?, + Some(v) => v.to_string() + }, + skin: match data.next() { + None => Err(GDError::PacketBad)?, + Some(v) => v.to_string() + }, + color_primary: match data.next() { + None => Err(GDError::PacketBad)?, + Some(v) => v.parse().map_err(|_| GDError::PacketBad)? + }, + color_secondary: match data.next() { + None => Err(GDError::PacketBad)?, + Some(v) => v.parse().map_err(|_| GDError::PacketBad)? + }, + }) + } +} + +pub fn query(address: &IpAddr, port: u16, timeout_settings: Option) -> GDResult> { + client_query::(address, port, timeout_settings) +} diff --git a/src/protocols/quake/three.rs b/src/protocols/quake/three.rs new file mode 100644 index 0000000..e2c2f27 --- /dev/null +++ b/src/protocols/quake/three.rs @@ -0,0 +1,28 @@ +use std::net::IpAddr; +use std::slice::Iter; +use crate::GDResult; +use crate::protocols::quake::two::{Player, QuakeTwo}; +use crate::protocols::quake::Response; +use crate::protocols::quake::client::{QuakeClient, client_query}; +use crate::protocols::types::TimeoutSettings; + +struct QuakeThree; +impl QuakeClient for QuakeThree { + type Player = Player; + + fn get_send_header<'a>() -> &'a str { + "getstatus" + } + + fn get_response_header<'a>() -> &'a [u8] { + &[0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6F, 0x6E, 0x73, 0x65, 0x0A] + } + + fn parse_player_string(data: Iter<&str>) -> GDResult { + QuakeTwo::parse_player_string(data) + } +} + +pub fn query(address: &IpAddr, port: u16, timeout_settings: Option) -> GDResult> { + client_query::(address, port, timeout_settings) +} diff --git a/src/protocols/quake/two.rs b/src/protocols/quake/two.rs new file mode 100644 index 0000000..ebc7e99 --- /dev/null +++ b/src/protocols/quake/two.rs @@ -0,0 +1,55 @@ +use std::net::IpAddr; +use std::slice::Iter; +use crate::{GDError, GDResult}; +use crate::protocols::quake::one::QuakeOne; +use crate::protocols::quake::Response; +use crate::protocols::quake::client::{QuakeClient, client_query}; +use crate::protocols::types::TimeoutSettings; +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +/// Quake 2 player data. +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub struct Player { + pub frags: u8, + pub ping: u8, + pub name: String +} + +pub(crate) struct QuakeTwo; +impl QuakeClient for QuakeTwo { + type Player = Player; + + fn get_send_header<'a>() -> &'a str { + QuakeOne::get_send_header() + } + + fn get_response_header<'a>() -> &'a [u8] { + &[0x70, 0x72, 0x69, 0x6E, 0x74, 0x0A] + } + + fn parse_player_string(mut data: Iter<&str>) -> GDResult { + Ok(Player { + frags: match data.next() { + None => Err(GDError::PacketBad)?, + Some(v) => v.parse().map_err(|_| GDError::PacketBad)? + }, + ping: match data.next() { + None => Err(GDError::PacketBad)?, + Some(v) => v.parse().map_err(|_| GDError::PacketBad)? + }, + name: match data.next() { + None => Err(GDError::PacketBad)?, + Some(v) => match v.starts_with('\"') && v.ends_with('\"') { + false => v, + true => &v[1..v.len() - 1] + }.to_string() + } + }) + } +} + +pub fn query(address: &IpAddr, port: u16, timeout_settings: Option) -> GDResult> { + client_query::(address, port, timeout_settings) +} diff --git a/src/protocols/quake/types.rs b/src/protocols/quake/types.rs new file mode 100644 index 0000000..486f916 --- /dev/null +++ b/src/protocols/quake/types.rs @@ -0,0 +1,29 @@ +use std::collections::HashMap; +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +/// General server information's. +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Response

{ + /// Name of the server. + pub name: String, + /// Map name. + pub map: String, + /// Current online players. + pub players: Vec

, + /// Number of players on the server. + pub players_online: u8, + /// Maximum number of players the server reports it can hold. + pub players_maximum: u8, + /// Indicates whether the server requires a password. + pub has_password: bool, + /// Maximum server frags. + pub frag_limit: u8, + /// Maximum server time. + pub time_limit: u8, + /// The server version. + pub version: String, + /// Other server entries that weren't used. + pub unused_entries: HashMap, +}