diff --git a/examples/tf2.rs b/examples/tf2.rs new file mode 100644 index 0000000..99c3741 --- /dev/null +++ b/examples/tf2.rs @@ -0,0 +1,14 @@ + +use gamedig::TF2; + +fn main() { + let response = TF2::query("5.15.202.107", None); + match response { + Err(_) => println!("fuck"), + Ok(r) => { + println!("{:?}", r); + + () + } + } +} diff --git a/src/errors.rs b/src/errors.rs new file mode 100644 index 0000000..1b0623e --- /dev/null +++ b/src/errors.rs @@ -0,0 +1,15 @@ +use core::fmt; +use std::fmt::Formatter; + +#[derive(Debug, Clone)] +pub enum GDError { + IDK(String) +} + +impl fmt::Display for GDError { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + GDError::IDK(details) => write!(f, "IDK: {details}") + } + } +} diff --git a/src/games/mod.rs b/src/games/mod.rs new file mode 100644 index 0000000..eee99b5 --- /dev/null +++ b/src/games/mod.rs @@ -0,0 +1,4 @@ + +pub mod tf2; + +pub use tf2::*; diff --git a/src/games/tf2.rs b/src/games/tf2.rs new file mode 100644 index 0000000..83c1452 --- /dev/null +++ b/src/games/tf2.rs @@ -0,0 +1,14 @@ +use crate::errors::GDError; +use crate::protocol::Protocol; +use crate::protocols::valve::{Response, ValveProtocol}; + +pub struct TF2; + +impl TF2 { + pub fn query(address: &str, port: Option) -> Result { + ValveProtocol::query(address, match port { + None => 27015, + Some(port) => port + }) + } +} diff --git a/src/lib.rs b/src/lib.rs index 7d12d9a..775b4e0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,14 +1,8 @@ -pub fn add(left: usize, right: usize) -> usize { - left + right -} -#[cfg(test)] -mod tests { - use super::*; +mod errors; +mod protocol; +mod protocols; +mod utils; +pub mod games; - #[test] - fn it_works() { - let result = add(2, 2); - assert_eq!(result, 4); - } -} +pub use games::*; diff --git a/src/protocol.rs b/src/protocol.rs new file mode 100644 index 0000000..cadc7cc --- /dev/null +++ b/src/protocol.rs @@ -0,0 +1,7 @@ +use crate::errors::GDError; + +pub trait Protocol { + type Response; + + fn query(address: &str, port: u16) -> Result; +} diff --git a/src/protocols/mod.rs b/src/protocols/mod.rs new file mode 100644 index 0000000..3deed53 --- /dev/null +++ b/src/protocols/mod.rs @@ -0,0 +1,2 @@ + +pub mod valve; diff --git a/src/protocols/valve.rs b/src/protocols/valve.rs new file mode 100644 index 0000000..5fa00cd --- /dev/null +++ b/src/protocols/valve.rs @@ -0,0 +1,137 @@ +use std::net::UdpSocket; +use crate::errors::GDError; +use crate::protocol::Protocol; +use crate::utils::{combine_two_u8, complete_address, concat_u8, find_null_in_array}; + +#[derive(Debug)] +pub enum Server { + Dedicated, + NonDedicated, + SourceTV +} + +#[derive(Debug)] +pub enum Environment { + Linux, + Windows, + Mac +} + +#[derive(Debug)] +pub struct Response { + pub protocol: u8, + pub map: String, + pub name: String, + pub folder: String, + pub game: String, + pub id: u16, + pub players: u8, + pub max_players: u8, + pub bots: u8, + pub server_type: Server, + pub environment_type: Environment, + pub has_password: bool, + pub vac_secured: bool +} + +pub enum Request { + A2sInfo(Option<[u8; 4]>) +} + +pub struct ValveProtocol { + socket: UdpSocket, + complete_address: String +} + +impl ValveProtocol { + fn new(address: &str, port: u16) -> Self { + Self { + socket: UdpSocket::bind("0.0.0.0:0").unwrap(), + complete_address: complete_address(address, port) + } + } + + pub fn do_request(&self, kind: Request, data_packet: Option<&[u8]>) -> bool { + let default: Vec = vec![0xFF, 0xFF, 0xFF, 0xFF, 0x54, 0x53, 0x6F, 0x75, 0x72, 0x63, 0x65, 0x20, + 0x45, 0x6E, 0x67, 0x69, 0x6E, 0x65, 0x20, 0x51, 0x75, 0x65, 0x72, 0x79, 0x00]; + + let request_kind_packet = match kind { + Request::A2sInfo(challenge) => match challenge { + None => default, + Some(value) => concat_u8(&default, &value) + } + }; + + let mut packet = request_kind_packet; + match data_packet { + None => (), + Some(data) => packet.extend_from_slice(data) + } + + match self.socket.send_to(&packet, &self.complete_address) { + Err(_) => false, + Ok(_) => true + } + } + + pub fn receive(&self) -> Vec { + self.receive_with_size(64) + } + + pub fn receive_with_size(&self, buffer_size: usize) -> Vec { + let mut buffer: Vec = vec![0; buffer_size]; + let (amt, _) = self.socket.recv_from(&mut buffer.as_mut_slice()).unwrap(); + buffer[..amt].to_vec() + } +} + +impl Protocol for ValveProtocol { + type Response = Response; + + fn query(address: &str, port: u16) -> Result { + let client = ValveProtocol::new(address, port); + + client.do_request(Request::A2sInfo(None), None); + let mut buf = client.receive(); + + if buf[4] == 0x41 { + client.do_request(Request::A2sInfo(Some([buf[5], buf[6], buf[7], buf[8]])), None); + } + + buf = client.receive_with_size(256); + println!("{:x?}", &buf); + + let name_nul_pos = find_null_in_array(&mut buf); + let map_nul_pos = name_nul_pos + 1 + find_null_in_array(&mut buf[name_nul_pos + 1..]); + let folder_nul_pos = map_nul_pos + 1 + find_null_in_array(&mut buf[map_nul_pos + 1..]); + let game_nul_pos = folder_nul_pos + 1 + find_null_in_array(&mut buf[folder_nul_pos + 1..]); + + let server_type = match buf[game_nul_pos + 6] as char { + 'd' => Server::Dedicated, + 'l' => Server::NonDedicated, + _ => Server::SourceTV + }; + + let environment_type = match buf[game_nul_pos + 7] as char { + 'l' => Environment::Linux, + 'w' => Environment::Windows, + _ => Environment::Mac + }; + + Ok(Response { + protocol: buf[5], + name: String::from_utf8(Vec::from(&mut buf[6..name_nul_pos])).expect("cacat"), + map: String::from_utf8(Vec::from(&mut buf[name_nul_pos + 1..map_nul_pos])).expect("cacat"), + folder: String::from_utf8(Vec::from(&mut buf[map_nul_pos + 1..folder_nul_pos])).expect("cacat"), + game: String::from_utf8(Vec::from(&mut buf[folder_nul_pos + 1..game_nul_pos])).expect("cacat"), + id: combine_two_u8(buf[game_nul_pos + 2], buf[game_nul_pos + 1]), + players: buf[game_nul_pos + 3], + max_players: buf[game_nul_pos + 4], + bots: buf[game_nul_pos + 5], + server_type, + environment_type, + has_password: buf[game_nul_pos + 8] == 1, + vac_secured: buf[game_nul_pos + 9] != 0 + }) + } +} diff --git a/src/utils.rs b/src/utils.rs new file mode 100644 index 0000000..c32b35c --- /dev/null +++ b/src/utils.rs @@ -0,0 +1,20 @@ +use std::ops::Add; + +pub fn concat_u8(first: &[u8], second: &[u8]) -> Vec { + [first, second].concat() +} + +pub fn find_null_in_array(arr: &[u8]) -> usize { + match arr.iter().position(|&x| x == 0) { + None => arr.len(), + Some(position) => position + } +} + +pub fn complete_address(address: &str, port: u16) -> String { + String::from(address.to_owned() + ":").add(&*port.to_string()) +} + +pub fn combine_two_u8(high: u8, low: u8) -> u16 { + ((high as u16) << 8) | low as u16 +} diff --git a/tests/test.rs b/tests/test.rs new file mode 100644 index 0000000..776744b --- /dev/null +++ b/tests/test.rs @@ -0,0 +1,15 @@ + +#[cfg(test)] +mod tests { + use gamedig::protocols::valve::ValveProtocol; + + #[test] + fn tf2() { + let response = ValveProtocol::query("5.15.202.107", 27015); + match response { + Err(_) => println!("fuck"), + Ok(r) => println!("{}", r.name) + } + assert_eq!(4, 4); + } +}