refator: copy cli into mono

This commit is contained in:
Cain 2023-10-16 23:20:47 +01:00
parent 66ae3c296e
commit 80f6b87991
63 changed files with 244 additions and 34 deletions

555
crates/lib/src/buffer.rs Normal file
View file

@ -0,0 +1,555 @@
use crate::GDErrorKind::PacketBad;
use crate::GDErrorKind::PacketUnderflow;
use crate::GDResult;
use byteorder::{BigEndian, ByteOrder, LittleEndian};
use std::{convert::TryInto, marker::PhantomData};
/// A struct representing a buffer with a specific byte order.
///
/// It's comprised of a byte slice that it reads from, a cursor to keep track of
/// the current position within the byte slice, and a `PhantomData` marker to
/// bind it to a specific byte order (BigEndian or LittleEndian).
///
/// The byte order is defined by the `B: ByteOrder` generic parameter.
pub struct Buffer<'a, B: ByteOrder> {
/// The byte slice that the buffer reads from.
data: &'a [u8],
/// The cursor marking our current position in the buffer.
cursor: usize,
/// A phantom field used to bind the `Buffer` to a specific `ByteOrder`.
_marker: PhantomData<B>,
}
impl<'a, B: ByteOrder> Buffer<'a, B> {
/// Creates and returns a new `Buffer` with the given data.
///
/// The cursor is set to the start of the buffer (position 0) upon
/// initialization.
///
/// # Arguments
///
/// * `data` - A byte slice that the buffer will read from.
pub const fn new(data: &'a [u8]) -> Self {
Self {
data,
cursor: 0,
_marker: PhantomData,
}
}
pub const fn current_position(&self) -> usize { self.cursor }
/// Returns the length of the remaining bytes from the current cursor
/// position.
pub const fn remaining_length(&self) -> usize { self.data.len() - self.cursor }
/// Returns the length of the buffer data.
pub const fn data_length(&self) -> usize { self.data.len() }
// TODO: Look into this to make it take ownership of data, not borrowing it
// There are many instances where we transform this to a vector.
/// Returns the remaining bytes that have not been read.
pub fn remaining_bytes(&self) -> &[u8] { &self.data[self.cursor ..] }
/// Moves the cursor forward or backward by a specified offset.
///
/// # Arguments
///
/// * `offset` - The amount to move the cursor. Use a negative value to move
/// backwards.
///
/// # Errors
///
/// Returns a `BufferError` if the attempted move would position the cursor
/// out of bounds.
pub fn move_cursor(&mut self, offset: isize) -> GDResult<()> {
// Compute the new cursor position by adding the offset to the current cursor
// position. The checked_add method is used for safe addition,
// preventing overflow and underflow.
let new_cursor = (self.cursor as isize).checked_add(offset);
match new_cursor {
// If the addition was not successful (i.e., it resulted in an overflow or underflow),
// return an error indicating that the cursor is out of bounds.
None => Err(PacketBad.into()),
// If the new cursor position is either less than zero (i.e., before the start of the buffer)
// or greater than the remaining length of the buffer (i.e., past the end of the buffer),
// return an error indicating that the cursor is out of bounds.
Some(x) if x < 0 || x as usize > self.data_length() => Err(PacketBad.into()),
// If the new cursor position is within the bounds of the buffer, update the cursor
// position and return Ok.
Some(x) => {
self.cursor = x as usize;
Ok(())
}
}
}
/// Reads a value of type `T` from the buffer, and advances the cursor by
/// the size of `T`.
///
/// # Type Parameters
///
/// * `T` - The type of value to be read from the buffer. This type must
/// implement the `BufferRead` trait with the same byte order as the
/// buffer.
///
/// # Errors
///
/// Returns a `BufferError` if there is not enough data remaining in the
/// buffer to read a value of type `T`.
pub fn read<T: Sized + BufferRead<B>>(&mut self) -> GDResult<T> {
// Get the size of `T` in bytes.
let size = std::mem::size_of::<T>();
// Calculate remaining length of the buffer.
let remaining = self.remaining_length();
// If the size of `T` is larger than the remaining length, return an error
// because we don't have enough data left to read.
if size > remaining {
return Err(PacketUnderflow.context(format!(
"Size requested {size} was larger than remaining bytes {remaining}"
)));
}
// Slice the data array from the current cursor position for `size` amount of
// bytes.
let bytes = &self.data[self.cursor .. self.cursor + size];
// Move the cursor forward by `size`.
self.cursor += size;
// Use the `read_from_buffer` function of the `BufferRead` implementation for
// `T` to convert the bytes into an instance of `T`.
T::read_from_buffer(bytes)
}
/// Reads a string from the buffer using a specified `StringDecoder`, until
/// an optional delimiter.
///
/// # Type Parameters
///
/// * `D` - The type of string decoder to use. This type must implement the
/// `StringDecoder` trait with the same byte order as the buffer.
///
/// # Arguments
///
/// * `until` - An optional delimiter. If provided, the method will read
/// until this
/// delimiter is encountered. If not provided, the method will read until
/// the default delimiter of the decoder.
///
/// # Errors
///
/// Returns a `BufferError` if there is an error decoding the string.
pub fn read_string<D: StringDecoder>(&mut self, until: Option<D::Delimiter>) -> GDResult<String> {
// Slice the data array from the current cursor position to the end.
let data_slice = &self.data[self.cursor ..];
// Use the provided delimiter if one was given, or default to the
// delimiter specified by the StringDecoder.
let delimiter = until.unwrap_or(D::DELIMITER);
// Invoke the decode_string function of the provided StringDecoder,
// passing in the remaining data slice, the mutable reference to the
// cursor, and the delimiter.
let result = D::decode_string(data_slice, &mut self.cursor, delimiter)?;
// If decoding was successful, return the decoded string. The cursor
// position has been updated within the decode_string call to reflect
// the new position after reading.
Ok(result)
}
}
/// A trait that provides an interface to switch endianness.
///
/// The trait `SwitchEndian` is used for types that have a specific
/// byte order (endianness) and can switch to another byte order.
/// The type of the switched endianness is determined by the associated
/// type `Output`.
///
/// The associated type `Output` must implement the `ByteOrder` trait.
pub trait SwitchEndian {
type Output: ByteOrder;
}
/// An implementation of `SwitchEndian` for `LittleEndian`.
///
/// The switched endianness type is `BigEndian`.
impl SwitchEndian for LittleEndian {
type Output = BigEndian;
}
/// An implementation of `SwitchEndian` for `BigEndian`.
///
/// The switched endianness type is `LittleEndian`.
impl SwitchEndian for BigEndian {
type Output = LittleEndian;
}
impl<'a, B: SwitchEndian + ByteOrder> Buffer<'a, B> {
/// Switches the byte order of a chunk in the buffer.
///
/// This method consumes the buffer and returns a new buffer
/// with a chunk of the original buffer's data, starting from the
/// original cursor position and of the given size, where the byte
/// order is switched according to the implementation
/// of `SwitchEndian` for `B`.
///
/// Note: The method also advances the cursor of the original buffer
/// by `size`.
///
/// # Parameters
///
/// * `size`: The size of the chunk to be taken from the original buffer.
pub fn switch_endian_chunk(&mut self, size: usize) -> GDResult<Buffer<'a, B::Output>> {
let old_cursor = self.cursor;
self.move_cursor(size as isize)?;
Ok(Buffer {
data: &self.data[old_cursor .. old_cursor + size],
cursor: 0,
_marker: PhantomData,
})
}
}
/// A trait defining a protocol for reading values of a certain type from a
/// buffer.
///
/// Implementors of this trait provide a method for reading their type from a
/// byte buffer with a specific byte order.
pub trait BufferRead<B: ByteOrder>: Sized {
fn read_from_buffer(data: &[u8]) -> GDResult<Self>;
}
/// Macro to implement the `BufferRead` trait for byte types.
///
/// This macro generates an implementation of the `BufferRead` trait for a
/// specified byte type. The implementation will read a single byte from the
/// buffer and convert it to the target type using the provided map function.
///
/// # Arguments
///
/// * `$type` - The target type to implement `BufferRead` for.
/// * `$map_func` - The function to map a byte to the target type.
macro_rules! impl_buffer_read_byte {
($type:ty, $map_func:expr) => {
impl<B: ByteOrder> BufferRead<B> for $type {
fn read_from_buffer(data: &[u8]) -> GDResult<Self> {
// Use the `first` method to get the first byte from the data array.
data.first()
// Apply the $map_func function to convert the raw byte to the $type.
.map($map_func)
// If the data array is empty (and thus `first` returns None),
// `ok_or_else` will return a BufferError.
.ok_or_else(|| PacketBad.into())
}
}
};
}
/// Macro to implement the `BufferRead` trait for multi-byte types.
///
/// This macro generates an implementation of the `BufferRead` trait for a
/// specified multi-byte type. The implementation will read the appropriate
/// number of bytes from the buffer and convert them to the target type using
/// the provided read function.
///
/// # Arguments
///
/// * `$type` - The target type to implement `BufferRead` for.
/// * `$read_func` - The function to read the bytes into the target type.
macro_rules! impl_buffer_read {
($type:ty, $read_func:ident) => {
impl<B: ByteOrder> BufferRead<B> for $type {
fn read_from_buffer(data: &[u8]) -> GDResult<Self> {
// Convert the byte slice into an array of the appropriate type.
let array = data.try_into().map_err(|e| {
// If conversion fails, return an error indicating the required and provided
// lengths.
PacketBad.context(e)
})?;
// Use the provided function to read the data from the array into the given
// type.
Ok(B::$read_func(array))
}
}
};
}
impl_buffer_read_byte!(u8, |&b| b);
impl_buffer_read_byte!(i8, |&b| b as i8);
impl_buffer_read!(u16, read_u16);
impl_buffer_read!(i16, read_i16);
impl_buffer_read!(u32, read_u32);
impl_buffer_read!(i32, read_i32);
impl_buffer_read!(u64, read_u64);
impl_buffer_read!(i64, read_i64);
impl_buffer_read!(f32, read_f32);
impl_buffer_read!(f64, read_f64);
/// A trait defining a protocol for decoding strings from a buffer.
///
/// This trait should be implemented by types that can decode strings from a
/// byte buffer with a specific byte order and delimiter.
pub trait StringDecoder {
/// The type of the delimiter used by the decoder.
type Delimiter: AsRef<[u8]>;
/// The default delimiter used by the decoder.
const DELIMITER: Self::Delimiter;
/// Decodes a string from the provided byte slice, and updates the cursor
/// position accordingly.
///
/// # Arguments
///
/// * `data` - The byte slice to decode the string from.
/// * `cursor` - The current position in the byte slice.
/// * `delimiter` - The delimiter to use for decoding the string.
///
/// # Errors
///
/// Returns a `BufferError` if there is an error decoding the string.
fn decode_string(data: &[u8], cursor: &mut usize, delimiter: Self::Delimiter) -> GDResult<String>;
}
/// A decoder for UTF-8 encoded strings.
///
/// This decoder uses a single null byte (`0x00`) as the default delimiter.
pub struct Utf8Decoder;
impl StringDecoder for Utf8Decoder {
type Delimiter = [u8; 1];
const DELIMITER: Self::Delimiter = [0x00];
/// Decodes a UTF-8 string from the given data, updating the cursor position
/// accordingly.
fn decode_string(data: &[u8], cursor: &mut usize, delimiter: Self::Delimiter) -> GDResult<String> {
// Find the position of the delimiter in the data. If the delimiter is not
// found, the length of the data is returned.
let position = data
// Create an iterator over the data.
.iter()
// Find the position of the delimiter
.position(|&b| b == delimiter.as_ref()[0])
// If the delimiter is not found, use the whole data slice.
.unwrap_or(data.len());
// Convert the data until the found position into a UTF-8 string.
let result = std::str::from_utf8(
// Take a slice of data until the position.
&data[.. position]
)
// If the data cannot be converted into a UTF-8 string, return an error
.map_err(|e| PacketBad.context(e))?
// Convert the resulting &str into a String
.to_owned();
// Update the cursor position
// The +1 is to skip the delimiter
*cursor += position + 1;
Ok(result)
}
}
/// A decoder for UTF-16 encoded strings.
///
/// This decoder uses a pair of null bytes (`0x00, 0x00`) as the default
/// delimiter.
///
/// # Type Parameters
///
/// * `B` - The byte order to use when decoding the string.
pub struct Utf16Decoder<B: ByteOrder> {
_marker: PhantomData<B>,
}
impl<B: ByteOrder> StringDecoder for Utf16Decoder<B> {
type Delimiter = [u8; 2];
const DELIMITER: Self::Delimiter = [0x00, 0x00];
/// Decodes a UTF-16 string from the given data, updating the cursor
/// position accordingly.
fn decode_string(data: &[u8], cursor: &mut usize, delimiter: Self::Delimiter) -> GDResult<String> {
// Try to find the position of the delimiter in the data
let position = data
// Split the data into 2-byte chunks (as UTF-16 uses 2 bytes per character)
.chunks_exact(2)
// Find the position of the delimiter
.position(|chunk| chunk == delimiter.as_ref())
// If the delimiter is not found, use the whole data, otherwise use the position of the delimiter
.map_or(data.len(), |pos| pos * 2);
// Create a buffer of u16 values to hold the decoded characters
let mut paired_buf: Vec<u16> = vec![0; position / 2];
// Decode the data into the buffer
B::read_u16_into(&data[.. position], &mut paired_buf);
// Convert the buffer of u16 values into a String
let result = String::from_utf16(&paired_buf).map_err(|e| PacketBad.context(e))?;
// Update the cursor position
// The +2 accounts for the delimiter
*cursor += position + 2;
Ok(result)
}
}
#[cfg(test)]
mod tests {
use super::*;
use byteorder::BigEndian;
#[test]
fn test_new_buffer() {
let data: &[u8] = &[1, 2, 3, 4];
let buffer = Buffer::<LittleEndian>::new(data);
assert_eq!(buffer.data, data);
assert_eq!(buffer.cursor, 0);
}
#[test]
fn test_remaining_length() {
let data: &[u8] = &[1, 2, 3, 4];
let mut buffer = Buffer::<LittleEndian>::new(data);
assert_eq!(buffer.remaining_length(), 4);
buffer.cursor = 2;
assert_eq!(buffer.remaining_length(), 2);
}
#[test]
fn test_move_cursor() {
let data: &[u8] = &[1, 2, 3, 4];
let mut buffer = Buffer::<LittleEndian>::new(data);
// Test moving forward
assert!(buffer.move_cursor(2).is_ok());
assert_eq!(buffer.cursor, 2);
// Test moving backward
assert!(buffer.move_cursor(-1).is_ok());
assert_eq!(buffer.cursor, 1);
// Test moving beyond data limits
assert!(buffer.move_cursor(5).is_err());
assert!(buffer.move_cursor(-2).is_err());
}
#[test]
fn test_switch_endian_chunk_le_be() {
let data = [0x01, 0x02, 0x03, 0x04];
let mut buffer = Buffer::<LittleEndian>::new(&data[..]);
let switched_buffer = buffer.switch_endian_chunk(2).unwrap();
assert_eq!(switched_buffer.data, [0x01, 0x02]);
assert_eq!(switched_buffer.cursor, 0);
assert_eq!(buffer.remaining_bytes(), [0x03, 0x04]);
assert_eq!(buffer.cursor, 2);
}
#[test]
fn test_switch_endian_chunk_be_le() {
let data = [0x01, 0x02, 0x03, 0x04];
let mut buffer = Buffer::<BigEndian>::new(&data[..]);
let switched_buffer = buffer.switch_endian_chunk(2).unwrap();
assert_eq!(switched_buffer.data, [0x01, 0x02]);
assert_eq!(switched_buffer.cursor, 0);
assert_eq!(buffer.remaining_bytes(), [0x03, 0x04]);
assert_eq!(buffer.cursor, 2);
}
#[test]
fn test_buffer_read_u8() {
let data: &[u8] = &[1, 2, 3, 4];
let mut buffer = Buffer::<LittleEndian>::new(data);
let result: Result<u8, _> = buffer.read();
assert_eq!(result.unwrap(), 1);
assert_eq!(buffer.cursor, 1);
}
#[test]
fn test_buffer_read_u16() {
let data: &[u8] = &[1, 2, 3, 4];
let mut buffer = Buffer::<LittleEndian>::new(data);
let result: Result<u16, _> = buffer.read();
assert_eq!(result.unwrap(), 0x0201);
assert_eq!(buffer.cursor, 2);
}
#[test]
fn test_buffer_read_u16_big_endian() {
let data: &[u8] = &[1, 2, 3, 4];
let mut buffer = Buffer::<BigEndian>::new(data);
let result: Result<u16, _> = buffer.read();
assert_eq!(result.unwrap(), 0x0102);
assert_eq!(buffer.cursor, 2);
}
#[test]
fn test_decode_string_utf8() {
let data: &[u8] = b"Hello\0World\0";
let mut cursor = 0;
let delimiter = [0x00];
let result = Utf8Decoder::decode_string(data, &mut cursor, delimiter);
assert_eq!(result.unwrap(), "Hello");
assert_eq!(cursor, 6);
}
#[test]
fn test_decode_string_utf16_le() {
let data: &[u8] = &[0x48, 0x00, 0x65, 0x00, 0x00, 0x00];
let mut cursor = 0;
let delimiter = [0x00, 0x00];
let result = Utf16Decoder::<LittleEndian>::decode_string(data, &mut cursor, delimiter);
assert_eq!(result.unwrap(), "He");
assert_eq!(cursor, 6);
}
#[test]
fn test_decode_string_utf16_be() {
let data: &[u8] = &[0x00, 0x48, 0x00, 0x65, 0x00, 0x00];
let mut cursor = 0;
let delimiter = [0x00, 0x00];
let result = Utf16Decoder::<BigEndian>::decode_string(data, &mut cursor, delimiter);
assert_eq!(result.unwrap(), "He");
assert_eq!(cursor, 6);
}
#[test]
fn test_buffer_underflow_error() {
let data: &[u8] = &[1, 2];
let mut buffer = Buffer::<LittleEndian>::new(data);
let result: Result<u32, _> = buffer.read();
assert_eq!(
result.unwrap_err(),
crate::GDErrorKind::PacketUnderflow.into()
);
}
}

View file

@ -0,0 +1,143 @@
use crate::GDErrorKind;
use std::error::Error;
use std::fmt::Formatter;
use std::{backtrace, fmt};
pub(crate) type ErrorSource = Box<dyn Error + 'static + Send + Sync>;
/// The GameDig error type.
///
/// Can be created in three ways (all of which will implicitly generate a
/// backtrace):
///
/// Directly from an [error kind](GDErrorKind) (without a
/// source).
///
/// ```
/// use gamedig::{GDError, GDErrorKind};
/// let _: GDError = GDErrorKind::PacketBad.into();
/// ```
///
/// [From an error kind with a source](GDErrorKind::context) (any
/// type that implements `Into<Box<dyn std::error::Error + 'static>>`).
///
/// ```
/// use gamedig::{GDError, GDErrorKind};
/// let _: GDError = GDErrorKind::PacketBad.context("Reason the packet was bad");
/// ```
///
/// Using the [new helper](GDError::new).
///
/// ```
/// use gamedig::{GDError, GDErrorKind};
/// let _: GDError = GDError::new(GDErrorKind::PacketBad, Some("Reason the packet was bad".into()));
/// ```
pub struct GDError {
pub kind: GDErrorKind,
pub source: Option<ErrorSource>,
pub backtrace: Option<backtrace::Backtrace>,
}
impl From<GDErrorKind> for GDError {
fn from(value: GDErrorKind) -> Self {
let backtrace = Some(backtrace::Backtrace::capture());
Self {
kind: value,
source: None,
backtrace,
}
}
}
impl PartialEq for GDError {
fn eq(&self, other: &Self) -> bool { self.kind == other.kind }
}
impl Error for GDError {
fn source(&self) -> Option<&(dyn Error + 'static)> { self.source.as_ref().map(|err| Box::as_ref(err) as _) }
}
impl fmt::Debug for GDError {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
writeln!(f, "GDError{{ kind={:?}", self.kind)?;
if let Some(source) = &self.source {
writeln!(f, " source={source:?}")?;
}
if let Some(backtrace) = &self.backtrace {
let bt = format!("{backtrace:#?}");
writeln!(f, " backtrace={}", bt.replace('\n', "\n "))?;
}
writeln!(f, "}}")?;
Ok(())
}
}
impl fmt::Display for GDError {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { write!(f, "{self:?}") }
}
impl GDError {
/// Create a new error (with automatic backtrace)
pub fn new(kind: GDErrorKind, source: Option<ErrorSource>) -> Self {
let backtrace = Some(backtrace::Backtrace::capture());
Self {
kind,
source,
backtrace,
}
}
/// Create a new error using any type that can be converted to an error
pub fn from_error<E: Into<ErrorSource>>(kind: GDErrorKind, source: E) -> Self {
Self::new(kind, Some(source.into()))
}
}
#[cfg(test)]
mod tests {
use super::*;
// test error trait GDError
#[test]
fn test_error_trait() {
let source: Result<u32, _> = "nan".parse();
let source_err = source.unwrap_err();
let error_with_context = GDErrorKind::TypeParse.context(source_err.clone());
assert!(error_with_context.source().is_some());
assert_eq!(
format!("{}", error_with_context.source().unwrap()),
format!("{source_err}")
);
let error_without_context: GDError = GDErrorKind::TypeParse.into();
assert!(error_without_context.source().is_none());
}
// Test creating GDError with GDError::new
#[test]
fn test_create_new() {
let error_from_new = GDError::new(GDErrorKind::InvalidInput, None);
assert!(error_from_new.backtrace.is_some());
assert_eq!(error_from_new.kind, GDErrorKind::InvalidInput);
assert!(error_from_new.source.is_none());
}
// Test creating GDError with GDErrorKind::context
#[test]
fn test_create_context() {
let error_from_context = GDErrorKind::InvalidInput.context("test");
assert!(error_from_context.backtrace.is_some());
assert_eq!(error_from_context.kind, GDErrorKind::InvalidInput);
assert!(error_from_context.source.is_some());
}
// Test creating GDError with From<GDErrorKind> for GDError
#[test]
fn test_create_into() {
let error_from_into: GDError = GDErrorKind::InvalidInput.into();
assert!(error_from_into.backtrace.is_some());
assert_eq!(error_from_into.kind, GDErrorKind::InvalidInput);
assert!(error_from_into.source.is_none());
}
}

View file

@ -0,0 +1,71 @@
use crate::error::ErrorSource;
use crate::GDError;
/// All GameDig Error kinds.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum GDErrorKind {
/// The received packet was bigger than the buffer size.
PacketOverflow,
/// The received packet was shorter than the expected one.
PacketUnderflow,
/// The received packet is badly formatted.
PacketBad,
/// Couldn't send the packet.
PacketSend,
/// Couldn't send the receive.
PacketReceive,
/// Couldn't decompress data.
Decompress,
/// Couldn't create a socket connection.
SocketConnect,
/// Couldn't bind a socket.
SocketBind,
/// Invalid input.
InvalidInput,
/// The server queried is not the queried game server.
BadGame,
/// Couldn't automatically query.
AutoQuery,
/// A protocol-defined expected format was not met.
ProtocolFormat,
/// Couldn't cast a value to an enum.
UnknownEnumCast,
/// Couldn't parse a json string.
JsonParse,
/// Couldn't parse a value.
TypeParse,
}
impl GDErrorKind {
/// Convert error kind into a full error with a source (and implicit
/// backtrace)
///
/// ```
/// use gamedig::{GDErrorKind, GDResult};
/// let _: GDResult<u32> = "thing".parse().map_err(|e| GDErrorKind::TypeParse.context(e));
/// ```
pub fn context<E: Into<ErrorSource>>(self, source: E) -> GDError { GDError::from_error(self, source) }
}
#[cfg(test)]
mod tests {
use super::*;
// Testing cloning the GDErrorKind type
#[test]
fn test_cloning() {
let error = GDErrorKind::BadGame;
let cloned_error = error.clone();
assert_eq!(error, cloned_error);
}
// test display GDError
#[test]
fn test_display() {
let err = GDErrorKind::BadGame.context("Rust is not a game");
assert_eq!(
format!("{err}"),
"GDError{ kind=BadGame\n source=\"Rust is not a game\"\n backtrace=<disabled>\n}\n"
);
}
}

View file

@ -0,0 +1,12 @@
//! Every GameDig errors.
/// The Error with backtrace.
pub mod error;
/// All defined Error kinds.
pub mod kind;
/// `GDResult`, a shorthand of `Result<T, GDError>`.
pub mod result;
pub use error::*;
pub use kind::*;
pub use result::*;

View file

@ -0,0 +1,24 @@
use crate::GDError;
/// `Result` of `T` and `GDError`.
pub type GDResult<T> = Result<T, GDError>;
#[cfg(test)]
mod tests {
use super::*;
use crate::GDErrorKind;
// Testing Ok variant of the GDResult type
#[test]
fn test_gdresult_ok() {
let result: GDResult<u32> = Ok(42);
assert_eq!(result.unwrap(), 42);
}
// Testing Err variant of the GDResult type
#[test]
fn test_gdresult_err() {
let result: GDResult<u32> = Err(GDErrorKind::InvalidInput.into());
assert!(result.is_err());
}
}

View file

@ -0,0 +1,46 @@
use crate::{
protocols::valve::{self, game, SteamApp},
GDErrorKind::TypeParse,
GDResult,
};
use std::net::{IpAddr, SocketAddr};
pub fn query(address: &IpAddr, port: Option<u16>) -> GDResult<game::Response> {
let mut valve_response = valve::query(
&SocketAddr::new(*address, port.unwrap_or(7780)),
SteamApp::BATTALION1944.as_engine(),
None,
None,
)?;
if let Some(rules) = &mut valve_response.rules {
if let Some(bat_max_players) = rules.get("bat_max_players_i") {
valve_response.info.players_maximum = bat_max_players.parse().map_err(|e| TypeParse.context(e))?;
rules.remove("bat_max_players_i");
}
if let Some(bat_player_count) = rules.get("bat_player_count_s") {
valve_response.info.players_online = bat_player_count.parse().map_err(|e| TypeParse.context(e))?;
rules.remove("bat_player_count_s");
}
if let Some(bat_has_password) = rules.get("bat_has_password_s") {
valve_response.info.has_password = bat_has_password == "Y";
rules.remove("bat_has_password_s");
}
if let Some(bat_name) = rules.get("bat_name_s") {
valve_response.info.name = bat_name.clone();
rules.remove("bat_name_s");
}
if let Some(bat_gamemode) = rules.get("bat_gamemode_s") {
valve_response.info.game_mode = bat_gamemode.clone();
rules.remove("bat_gamemode_s");
}
rules.remove("bat_map_s");
}
Ok(game::Response::new_from_valve_response(valve_response))
}

View file

@ -0,0 +1,92 @@
//! Static definitions of currently supported games
use crate::protocols::{
gamespy::GameSpyVersion,
minecraft::{LegacyGroup, Server},
quake::QuakeVersion,
valve::SteamApp,
Protocol,
};
use crate::Game;
use crate::protocols::types::ProprietaryProtocol;
use phf::{phf_map, Map};
macro_rules! game {
($name: literal, $default_port: literal, $protocol: expr) => {
Game {
name: $name,
default_port: $default_port,
protocol: $protocol,
}
};
}
/// Map of all currently supported games
pub static GAMES: Map<&'static str, Game> = phf_map! {
// Query with all minecraft protocols
"minecraft" => game!("Minecraft", 25565, Protocol::Minecraft(None)),
// Query with specific minecraft protocols
"minecraftbedrock" => game!("Minecraft (bedrock)", 19132, Protocol::Minecraft(Some(Server::Bedrock))),
"minecraftpocket" => game!("Minecraft (pocket)", 19132, Protocol::Minecraft(Some(Server::Bedrock))),
"minecraftjava" => game!("Minecraft (java)", 25565, Protocol::Minecraft(Some(Server::Java))),
"minecraftlegacy16" => game!("Minecraft (legacy v1.6)", 25565, Protocol::Minecraft(Some(Server::Legacy(LegacyGroup::V1_6)))),
"minecraftlegacy15" => game!("Minecraft (legacy v1.4-1.5)", 25565, Protocol::Minecraft(Some(Server::Legacy(LegacyGroup::V1_5)))),
"minecraftlegacy13" => game!("Minecraft (legacy vB1.8-1.3)", 25565, Protocol::Minecraft(Some(Server::Legacy(LegacyGroup::V1_3)))),
"alienswarm" => game!("Alien Swarm", 27015, Protocol::Valve(SteamApp::ALIENSWARM)),
"aoc" => game!("Age of Chivalry", 27015, Protocol::Valve(SteamApp::AOC)),
"a2oa" => game!("ARMA 2: Operation Arrowhead", 2304, Protocol::Valve(SteamApp::A2OA)),
"ase" => game!("ARK: Survival Evolved", 27015, Protocol::Valve(SteamApp::ASE)),
"asrd" => game!("Alien Swarm: Reactive Drop", 2304, Protocol::Valve(SteamApp::ASRD)),
"avorion" => game!("Avorion", 27020, Protocol::Valve(SteamApp::AVORION)),
"barotrauma" => game!("Barotrauma", 27016, Protocol::Valve(SteamApp::BAROTRAUMA)),
"battalion1944" => game!("Battalion 1944", 7780, Protocol::Valve(SteamApp::BATTALION1944)),
"brainbread2" => game!("BrainBread 2", 27015, Protocol::Valve(SteamApp::BRAINBREAD2)),
"battlefield1942" => game!("Battlefield 1942", 23000, Protocol::Gamespy(GameSpyVersion::One)),
"blackmesa" => game!("Black Mesa", 27015, Protocol::Valve(SteamApp::BLACKMESA)),
"ballisticoverkill" => game!("Ballistic Overkill", 27016, Protocol::Valve(SteamApp::BALLISTICOVERKILL)),
"codenamecure" => game!("Codename CURE", 27015, Protocol::Valve(SteamApp::CODENAMECURE)),
"colonysurvival" => game!("Colony Survival", 27004, Protocol::Valve(SteamApp::COLONYSURVIVAL)),
"counterstrike" => game!("Counter-Strike", 27015, Protocol::Valve(SteamApp::COUNTERSTRIKE)),
"cscz" => game!("Counter Strike: Condition Zero", 27015, Protocol::Valve(SteamApp::CSCZ)),
"csgo" => game!("Counter-Strike: Global Offensive", 27015, Protocol::Valve(SteamApp::CSGO)),
"css" => game!("Counter-Strike: Source", 27015, Protocol::Valve(SteamApp::CSS)),
"creativerse" => game!("Creativerse", 26901, Protocol::Valve(SteamApp::CREATIVERSE)),
"crysiswars" => game!("Crysis Wars", 64100, Protocol::Gamespy(GameSpyVersion::Three)),
"dod" => game!("Day of Defeat", 27015, Protocol::Valve(SteamApp::DOD)),
"dods" => game!("Day of Defeat: Source", 27015, Protocol::Valve(SteamApp::DODS)),
"doi" => game!("Day of Infamy", 27015, Protocol::Valve(SteamApp::DOI)),
"dst" => game!("Don't Starve Together", 27016, Protocol::Valve(SteamApp::DST)),
"ffow" => game!("Frontlines: Fuel of War", 5478, Protocol::PROPRIETARY(ProprietaryProtocol::FFOW)),
"garrysmod" => game!("Garry's Mod", 27016, Protocol::Valve(SteamApp::GARRYSMOD)),
"hl2d" => game!("Half-Life 2 Deathmatch", 27015, Protocol::Valve(SteamApp::HL2D)),
"hce" => game!("Halo: Combat Evolved", 2302, Protocol::Gamespy(GameSpyVersion::Two)),
"hlds" => game!("Half-Life Deathmatch: Source", 27015, Protocol::Valve(SteamApp::HLDS)),
"hll" => game!("Hell Let Loose", 26420, Protocol::Valve(SteamApp::HLL)),
"insurgency" => game!("Insurgency", 27015, Protocol::Valve(SteamApp::INSURGENCY)),
"imic" => game!("Insurgency: Modern Infantry Combat", 27015, Protocol::Valve(SteamApp::IMIC)),
"insurgencysandstorm" => game!("Insurgency: Sandstorm", 27131, Protocol::Valve(SteamApp::INSURGENCYSANDSTORM)),
"left4dead" => game!("Left 4 Dead", 27015, Protocol::Valve(SteamApp::LEFT4DEAD)),
"left4dead2" => game!("Left 4 Dead 2", 27015, Protocol::Valve(SteamApp::LEFT4DEAD2)),
"ohd" => game!("Operation: Harsh Doorstop", 27005, Protocol::Valve(SteamApp::OHD)),
"onset" => game!("Onset", 7776, Protocol::Valve(SteamApp::ONSET)),
"projectzomboid" => game!("Project Zomboid", 16261, Protocol::Valve(SteamApp::PROJECTZOMBOID)),
"quake1" => game!("Quake 1", 27500, Protocol::Quake(QuakeVersion::One)),
"quake2" => game!("Quake 2", 27910, Protocol::Quake(QuakeVersion::Two)),
"quake3" => game!("Quake 3: Arena", 27960, Protocol::Quake(QuakeVersion::Three)),
"ror2" => game!("Risk of Rain 2", 27016, Protocol::Valve(SteamApp::ROR2)),
"rust" => game!("Rust", 27015, Protocol::Valve(SteamApp::RUST)),
"sco" => game!("Sven Co-op", 27015, Protocol::Valve(SteamApp::SCO)),
"7d2d" => game!("7 Days To Die", 26900, Protocol::Valve(SteamApp::SD2D)),
"sof2" => game!("Soldier of Fortune 2", 20100, Protocol::Quake(QuakeVersion::Three)),
"serioussam" => game!("Serious Sam", 25601, Protocol::Gamespy(GameSpyVersion::One)),
"theforest" => game!("The Forest", 27016, Protocol::Valve(SteamApp::THEFOREST)),
"teamfortress2" => game!("Team Fortress 2", 27015, Protocol::Valve(SteamApp::TEAMFORTRESS2)),
"tfc" => game!("Team Fortress Classic", 27015, Protocol::Valve(SteamApp::TFC)),
"theship" => game!("The Ship", 27015, Protocol::PROPRIETARY(ProprietaryProtocol::TheShip)),
"unturned" => game!("Unturned", 27015, Protocol::Valve(SteamApp::UNTURNED)),
"unrealtournament" => game!("Unreal Tournament", 7778, Protocol::Gamespy(GameSpyVersion::One)),
"vrising" => game!("V Rising", 27016, Protocol::Valve(SteamApp::VRISING)),
"jc2m" => game!("Just Cause 2: Multiplayer", 7777, Protocol::PROPRIETARY(ProprietaryProtocol::JC2M)),
"warsow" => game!("Warsow", 44400, Protocol::Quake(QuakeVersion::Three)),
};

View file

@ -0,0 +1,119 @@
use crate::buffer::{Buffer, Utf8Decoder};
use crate::protocols::types::{CommonResponse, TimeoutSettings};
use crate::protocols::valve::{Engine, Environment, Server, ValveProtocol};
use crate::protocols::GenericResponse;
use crate::GDResult;
use byteorder::LittleEndian;
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
use std::net::{IpAddr, SocketAddr};
/// The query response.
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct Response {
/// Protocol used by the server.
pub protocol_version: u8,
/// Name of the server.
pub name: String,
/// Map name.
pub active_mod: String,
/// Running game mode.
pub game_mode: String,
/// The version that the server is running on.
pub game_version: String,
/// Description of the server.
pub description: String,
/// Current map.
pub map: String,
/// Number of players on the server.
pub players_online: u8,
/// Maximum number of players the server reports it can hold.
pub players_maximum: u8,
/// Dedicated, NonDedicated or SourceTV
pub server_type: Server,
/// The Operating System that the server is on.
pub environment_type: Environment,
/// Indicates whether the server requires a password.
pub has_password: bool,
/// Indicates whether the server uses VAC.
pub vac_secured: bool,
/// Current round index.
pub round: u8,
/// Maximum amount of rounds.
pub rounds_maximum: u8,
/// Time left for the current round in seconds.
pub time_left: u16,
}
impl CommonResponse for Response {
fn as_original(&self) -> GenericResponse { GenericResponse::FFOW(self) }
fn name(&self) -> Option<&str> { Some(&self.name) }
fn game_mode(&self) -> Option<&str> { Some(&self.game_mode) }
fn description(&self) -> Option<&str> { Some(&self.description) }
fn game_version(&self) -> Option<&str> { Some(&self.game_version) }
fn map(&self) -> Option<&str> { Some(&self.map) }
fn has_password(&self) -> Option<bool> { Some(self.has_password) }
fn players_maximum(&self) -> u32 { self.players_maximum.into() }
fn players_online(&self) -> u32 { self.players_online.into() }
}
pub fn query(address: &IpAddr, port: Option<u16>) -> GDResult<Response> { query_with_timeout(address, port, None) }
pub fn query_with_timeout(
address: &IpAddr,
port: Option<u16>,
timeout_settings: Option<TimeoutSettings>,
) -> GDResult<Response> {
let mut client = ValveProtocol::new(
&SocketAddr::new(*address, port.unwrap_or(5478)),
timeout_settings,
)?;
let data = client.get_request_data(
&Engine::GoldSrc(true),
0,
0x46,
String::from("LSQ").into_bytes(),
)?;
let mut buffer = Buffer::<LittleEndian>::new(&data);
let protocol_version = buffer.read::<u8>()?;
let name = buffer.read_string::<Utf8Decoder>(None)?;
let map = buffer.read_string::<Utf8Decoder>(None)?;
let active_mod = buffer.read_string::<Utf8Decoder>(None)?;
let game_mode = buffer.read_string::<Utf8Decoder>(None)?;
let description = buffer.read_string::<Utf8Decoder>(None)?;
let game_version = buffer.read_string::<Utf8Decoder>(None)?;
buffer.move_cursor(2)?;
let players_online = buffer.read::<u8>()?;
let players_maximum = buffer.read::<u8>()?;
let server_type = Server::from_gldsrc(buffer.read::<u8>()?)?;
let environment_type = Environment::from_gldsrc(buffer.read::<u8>()?)?;
let has_password = buffer.read::<u8>()? == 1;
let vac_secured = buffer.read::<u8>()? == 1;
buffer.move_cursor(1)?; //average fps
let round = buffer.read::<u8>()?;
let rounds_maximum = buffer.read::<u8>()?;
let time_left = buffer.read::<u16>()?;
Ok(Response {
protocol_version,
name,
active_mod,
game_mode,
game_version,
description,
map,
players_online,
players_maximum,
server_type,
environment_type,
has_password,
vac_secured,
round,
rounds_maximum,
time_left,
})
}

View file

@ -0,0 +1,9 @@
//! Gamespy game query modules
use crate::protocols::gamespy::game_query_mod;
game_query_mod!(battlefield1942, "Battlefield 1942", one, 23000);
game_query_mod!(crysiswars, "Crysis Wars", three, 64100);
game_query_mod!(hce, "Halo: Combat Evolved", two, 2302);
game_query_mod!(serioussam, "Serious Sam", one, 25601);
game_query_mod!(unrealtournament, "Unreal Tournament", one, 7778);

View file

@ -0,0 +1,129 @@
use crate::buffer::{Buffer, Utf8Decoder};
use crate::protocols::gamespy::common::has_password;
use crate::protocols::gamespy::three::{data_to_map, GameSpy3};
use crate::protocols::types::{CommonPlayer, CommonResponse, GenericPlayer, TimeoutSettings};
use crate::protocols::GenericResponse;
use crate::GDErrorKind::{PacketBad, TypeParse};
use crate::{GDErrorKind, GDResult};
use byteorder::BigEndian;
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
use std::net::{IpAddr, SocketAddr};
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct Player {
name: String,
steam_id: String,
ping: u16,
}
impl CommonPlayer for Player {
fn as_original(&self) -> GenericPlayer { GenericPlayer::JCMP2(self) }
fn name(&self) -> &str { &self.name }
}
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct Response {
game_version: String,
description: String,
name: String,
has_password: bool,
players: Vec<Player>,
players_maximum: u32,
players_online: u32,
}
impl CommonResponse for Response {
fn as_original(&self) -> GenericResponse { GenericResponse::JC2M(self) }
fn game_version(&self) -> Option<&str> { Some(&self.game_version) }
fn description(&self) -> Option<&str> { Some(&self.description) }
fn name(&self) -> Option<&str> { Some(&self.name) }
fn has_password(&self) -> Option<bool> { Some(self.has_password) }
fn players_maximum(&self) -> u32 { self.players_maximum }
fn players_online(&self) -> u32 { self.players_online }
fn players(&self) -> Option<Vec<&dyn crate::protocols::types::CommonPlayer>> {
Some(
self.players
.iter()
.map(|p| p as &dyn CommonPlayer)
.collect(),
)
}
}
fn parse_players_and_teams(packet: &[u8]) -> GDResult<Vec<Player>> {
let mut buf = Buffer::<BigEndian>::new(packet);
let count = buf.read::<u16>()?;
let mut players = Vec::with_capacity(count as usize);
while buf.remaining_length() != 0 {
players.push(Player {
name: buf.read_string::<Utf8Decoder>(None)?,
steam_id: buf.read_string::<Utf8Decoder>(None)?,
ping: buf.read::<u16>()?,
});
}
Ok(players)
}
pub fn query(address: &IpAddr, port: Option<u16>) -> GDResult<Response> { query_with_timeout(address, port, None) }
pub fn query_with_timeout(
address: &IpAddr,
port: Option<u16>,
timeout_settings: Option<TimeoutSettings>,
) -> GDResult<Response> {
let mut client = GameSpy3::new_custom(
&SocketAddr::new(*address, port.unwrap_or(7777)),
timeout_settings,
[0xFF, 0xFF, 0xFF, 0x02],
true,
)?;
let packets = client.get_server_packets()?;
let data = packets
.get(0)
.ok_or(PacketBad.context("First packet missing"))?;
let (mut server_vars, remaining_data) = data_to_map(data)?;
let players = parse_players_and_teams(&remaining_data)?;
let players_maximum = server_vars
.remove("maxplayers")
.ok_or(PacketBad.context("Server variables missing maxplayers"))?
.parse()
.map_err(|e| TypeParse.context(e))?;
let players_online = match server_vars.remove("numplayers") {
None => players.len(),
Some(v) => {
let reported_players = v.parse().map_err(|e| TypeParse.context(e))?;
match reported_players < players.len() {
true => players.len(),
false => reported_players,
}
}
} as u32;
Ok(Response {
game_version: server_vars
.remove("version")
.ok_or(GDErrorKind::PacketBad)?,
description: server_vars
.remove("description")
.ok_or(GDErrorKind::PacketBad)?,
name: server_vars
.remove("hostname")
.ok_or(GDErrorKind::PacketBad)?,
has_password: has_password(&mut server_vars)?,
players,
players_maximum,
players_online,
})
}

View file

@ -0,0 +1,64 @@
use crate::protocols::minecraft::RequestSettings;
use crate::{
protocols::minecraft::{self, BedrockResponse, JavaResponse, LegacyGroup},
GDErrorKind,
GDResult,
};
use std::net::{IpAddr, SocketAddr};
/// Query with all the protocol variants one by one (Java -> Bedrock -> Legacy
/// (1.6 -> 1.4 -> Beta 1.8)).
pub fn query(address: &IpAddr, port: Option<u16>) -> GDResult<JavaResponse> {
if let Ok(response) = query_java(address, port, None) {
return Ok(response);
}
if let Ok(response) = query_bedrock(address, port) {
return Ok(JavaResponse::from_bedrock_response(response));
}
if let Ok(response) = query_legacy(address, port) {
return Ok(response);
}
Err(GDErrorKind::AutoQuery.into())
}
/// Query a Java Server.
pub fn query_java(
address: &IpAddr,
port: Option<u16>,
request_settings: Option<RequestSettings>,
) -> GDResult<JavaResponse> {
minecraft::query_java(
&SocketAddr::new(*address, port_or_java_default(port)),
None,
request_settings,
)
}
/// Query a (Java) Legacy Server (1.6 -> 1.4 -> Beta 1.8).
pub fn query_legacy(address: &IpAddr, port: Option<u16>) -> GDResult<JavaResponse> {
minecraft::query_legacy(&SocketAddr::new(*address, port_or_java_default(port)), None)
}
/// Query a specific (Java) Legacy Server.
pub fn query_legacy_specific(group: LegacyGroup, address: &IpAddr, port: Option<u16>) -> GDResult<JavaResponse> {
minecraft::query_legacy_specific(
group,
&SocketAddr::new(*address, port_or_java_default(port)),
None,
)
}
/// Query a Bedrock Server.
pub fn query_bedrock(address: &IpAddr, port: Option<u16>) -> GDResult<BedrockResponse> {
minecraft::query_bedrock(
&SocketAddr::new(*address, port_or_bedrock_default(port)),
None,
)
}
fn port_or_java_default(port: Option<u16>) -> u16 { port.unwrap_or(25565) }
fn port_or_bedrock_default(port: Option<u16>) -> u16 { port.unwrap_or(19132) }

138
crates/lib/src/games/mod.rs Normal file
View file

@ -0,0 +1,138 @@
//! Currently supported games.
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
pub mod gamespy;
pub mod quake;
pub mod valve;
pub use gamespy::*;
pub use quake::*;
pub use valve::*;
/// Battalion 1944
pub mod battalion1944;
/// Frontlines: Fuel of War
pub mod ffow;
/// Just Cause 2: Multiplayer
pub mod jc2m;
/// Minecraft
pub mod minecraft;
/// The Ship
pub mod theship;
use crate::protocols::gamespy::GameSpyVersion;
use crate::protocols::quake::QuakeVersion;
use crate::protocols::types::{CommonResponse, ExtraRequestSettings, ProprietaryProtocol, TimeoutSettings};
use crate::protocols::{self, Protocol};
use crate::GDResult;
use std::net::{IpAddr, SocketAddr};
/// Definition of a game
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Game {
/// Full name of the game
pub name: &'static str,
/// Default port used by game
pub default_port: u16,
/// The protocol the game's query uses
pub protocol: Protocol,
}
#[cfg(feature = "game_defs")]
mod definitions;
#[cfg(feature = "game_defs")]
pub use definitions::GAMES;
/// Make a query given a game definition
#[inline]
pub fn query(game: &Game, address: &IpAddr, port: Option<u16>) -> GDResult<Box<dyn CommonResponse>> {
query_with_timeout_and_extra_settings(game, address, port, None, None)
}
/// Make a query given a game definition and timeout settings
#[inline]
pub fn query_with_timeout(
game: &Game,
address: &IpAddr,
port: Option<u16>,
timeout_settings: Option<TimeoutSettings>,
) -> GDResult<Box<dyn CommonResponse>> {
query_with_timeout_and_extra_settings(game, address, port, timeout_settings, None)
}
/// Make a query given a game definition, timeout settings, and extra settings
pub fn query_with_timeout_and_extra_settings(
game: &Game,
address: &IpAddr,
port: Option<u16>,
timeout_settings: Option<TimeoutSettings>,
extra_settings: Option<ExtraRequestSettings>,
) -> GDResult<Box<dyn CommonResponse>> {
let socket_addr = SocketAddr::new(*address, port.unwrap_or(game.default_port));
Ok(match &game.protocol {
Protocol::Valve(steam_app) => {
protocols::valve::query(
&socket_addr,
steam_app.as_engine(),
extra_settings.map(ExtraRequestSettings::into),
timeout_settings,
)
.map(Box::new)?
}
Protocol::Minecraft(version) => {
match version {
Some(protocols::minecraft::Server::Java) => {
protocols::minecraft::query_java(
&socket_addr,
timeout_settings,
extra_settings.map(ExtraRequestSettings::into),
)
.map(Box::new)?
}
Some(protocols::minecraft::Server::Bedrock) => {
protocols::minecraft::query_bedrock(&socket_addr, timeout_settings).map(Box::new)?
}
Some(protocols::minecraft::Server::Legacy(group)) => {
protocols::minecraft::query_legacy_specific(*group, &socket_addr, timeout_settings).map(Box::new)?
}
None => {
protocols::minecraft::query(
&socket_addr,
timeout_settings,
extra_settings.map(ExtraRequestSettings::into),
)
.map(Box::new)?
}
}
}
Protocol::Gamespy(version) => {
match version {
GameSpyVersion::One => protocols::gamespy::one::query(&socket_addr, timeout_settings).map(Box::new)?,
GameSpyVersion::Two => protocols::gamespy::two::query(&socket_addr, timeout_settings).map(Box::new)?,
GameSpyVersion::Three => {
protocols::gamespy::three::query(&socket_addr, timeout_settings).map(Box::new)?
}
}
}
Protocol::Quake(version) => {
match version {
QuakeVersion::One => protocols::quake::one::query(&socket_addr, timeout_settings).map(Box::new)?,
QuakeVersion::Two => protocols::quake::two::query(&socket_addr, timeout_settings).map(Box::new)?,
QuakeVersion::Three => protocols::quake::three::query(&socket_addr, timeout_settings).map(Box::new)?,
}
}
Protocol::PROPRIETARY(protocol) => {
match protocol {
ProprietaryProtocol::TheShip => {
theship::query_with_timeout(address, port, timeout_settings).map(Box::new)?
}
ProprietaryProtocol::FFOW => ffow::query_with_timeout(address, port, timeout_settings).map(Box::new)?,
ProprietaryProtocol::JC2M => jc2m::query_with_timeout(address, port, timeout_settings).map(Box::new)?,
}
}
})
}

View file

@ -0,0 +1,9 @@
//! Quake game query modules
use crate::protocols::quake::game_query_mod;
game_query_mod!(quake1, "Quake 1", one, 27500);
game_query_mod!(quake2, "Quake 2", two, 27910);
game_query_mod!(quake3, "Quake 3: Arena", three, 27960);
game_query_mod!(sof2, "Soldier of Fortune 2", three, 20100);
game_query_mod!(warsow, "Warsow", three, 44400);

View file

@ -0,0 +1,145 @@
use crate::{
protocols::{
types::{CommonPlayer, CommonResponse, GenericPlayer, TimeoutSettings},
valve::{self, get_optional_extracted_data, Server, ServerPlayer, SteamApp},
GenericResponse,
},
GDErrorKind::PacketBad,
GDResult,
};
use std::net::{IpAddr, SocketAddr};
use std::collections::HashMap;
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq, PartialOrd)]
pub struct TheShipPlayer {
pub name: String,
pub score: i32,
pub duration: f32,
pub deaths: u32,
pub money: u32,
}
impl TheShipPlayer {
pub fn new_from_valve_player(player: &ServerPlayer) -> GDResult<Self> {
Ok(Self {
name: player.name.clone(),
score: player.score,
duration: player.duration,
deaths: player.deaths.ok_or(PacketBad)?,
money: player.money.ok_or(PacketBad)?,
})
}
}
impl CommonPlayer for TheShipPlayer {
fn as_original(&self) -> GenericPlayer { GenericPlayer::TheShip(self) }
fn name(&self) -> &str { &self.name }
fn score(&self) -> Option<i32> { Some(self.score) }
}
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq)]
pub struct Response {
pub protocol_version: u8,
pub name: String,
pub map: String,
pub game_mode: String,
pub game_version: String,
pub players: Vec<TheShipPlayer>,
pub players_online: u8,
pub players_maximum: u8,
pub players_bots: u8,
pub server_type: Server,
pub has_password: bool,
pub vac_secured: bool,
pub port: Option<u16>,
pub steam_id: Option<u64>,
pub tv_port: Option<u16>,
pub tv_name: Option<String>,
pub keywords: Option<String>,
pub rules: HashMap<String, String>,
pub mode: u8,
pub witnesses: u8,
pub duration: u8,
}
impl CommonResponse for Response {
fn as_original(&self) -> GenericResponse { GenericResponse::TheShip(self) }
fn name(&self) -> Option<&str> { Some(&self.name) }
fn map(&self) -> Option<&str> { Some(&self.map) }
fn game_mode(&self) -> Option<&str> { Some(&self.game_mode) }
fn players_maximum(&self) -> u32 { self.players_maximum.into() }
fn players_online(&self) -> u32 { self.players_online.into() }
fn players_bots(&self) -> Option<u32> { Some(self.players_bots.into()) }
fn has_password(&self) -> Option<bool> { Some(self.has_password) }
fn players(&self) -> Option<Vec<&dyn CommonPlayer>> {
Some(
self.players
.iter()
.map(|p| p as &dyn CommonPlayer)
.collect(),
)
}
}
impl Response {
pub fn new_from_valve_response(response: valve::Response) -> GDResult<Self> {
let (port, steam_id, tv_port, tv_name, keywords) = get_optional_extracted_data(response.info.extra_data);
let the_unwrapped_ship = response.info.the_ship.ok_or(PacketBad)?;
Ok(Self {
protocol_version: response.info.protocol_version,
name: response.info.name,
map: response.info.map,
game_mode: response.info.game_mode,
game_version: response.info.game_version,
players_online: response.info.players_online,
players: response
.players
.ok_or(PacketBad)?
.iter()
.map(TheShipPlayer::new_from_valve_player)
.collect::<GDResult<Vec<TheShipPlayer>>>()?,
players_maximum: response.info.players_maximum,
players_bots: response.info.players_bots,
server_type: response.info.server_type,
has_password: response.info.has_password,
vac_secured: response.info.vac_secured,
port,
steam_id,
tv_port,
tv_name,
keywords,
rules: response.rules.ok_or(PacketBad)?,
mode: the_unwrapped_ship.mode,
witnesses: the_unwrapped_ship.witnesses,
duration: the_unwrapped_ship.duration,
})
}
}
pub fn query(address: &IpAddr, port: Option<u16>) -> GDResult<Response> { query_with_timeout(address, port, None) }
pub fn query_with_timeout(
address: &IpAddr,
port: Option<u16>,
timeout_settings: Option<TimeoutSettings>,
) -> GDResult<Response> {
let valve_response = valve::query(
&SocketAddr::new(*address, port.unwrap_or(27015)),
SteamApp::THESHIP.as_engine(),
None,
timeout_settings,
)?;
Response::new_from_valve_response(valve_response)
}

View file

@ -0,0 +1,56 @@
//! Valve game query modules
use crate::protocols::valve::game_query_mod;
game_query_mod!(a2oa, "ARMA 2: Operation Arrowhead", A2OA, 2304);
game_query_mod!(alienswarm, "Alien Swarm", ALIENSWARM, 27015);
game_query_mod!(aoc, "Age of Chivalry", AOC, 27015);
game_query_mod!(ase, "ARK: Survival Evolved", ASE, 27015);
game_query_mod!(asrd, "Alien Swarm: Reactive Drop", ASRD, 2304);
game_query_mod!(avorion, "Avorion", AVORION, 27020);
game_query_mod!(
ballisticoverkill,
"Ballistic Overkill",
BALLISTICOVERKILL,
27016
);
game_query_mod!(barotrauma, "Barotrauma", BAROTRAUMA, 27016);
game_query_mod!(blackmesa, "Black Mesa", BLACKMESA, 27015);
game_query_mod!(brainbread2, "BrainBread 2", BRAINBREAD2, 27015);
game_query_mod!(codenamecure, "Codename CURE", CODENAMECURE, 27015);
game_query_mod!(colonysurvival, "Colony Survival", COLONYSURVIVAL, 27004);
game_query_mod!(counterstrike, "Counter-Strike", COUNTERSTRIKE, 27015);
game_query_mod!(creativerse, "Creativerse", CREATIVERSE, 26901);
game_query_mod!(cscz, "Counter Strike: Condition Zero", CSCZ, 27015);
game_query_mod!(csgo, "Counter-Strike: Global Offensive", CSGO, 27015);
game_query_mod!(css, "Counter-Strike: Source", CSS, 27015);
game_query_mod!(dod, "Day of Defeat", DOD, 27015);
game_query_mod!(dods, "Day of Defeat: Source", DODS, 27015);
game_query_mod!(doi, "Day of Infamy", DOI, 27015);
game_query_mod!(dst, "Don't Starve Together", DST, 27016);
game_query_mod!(garrysmod, "Garry's Mod", GARRYSMOD, 27016);
game_query_mod!(hl2d, "Half-Life 2 Deathmatch", HL2D, 27015);
game_query_mod!(hlds, "Half-Life Deathmatch: Source", HLDS, 27015);
game_query_mod!(hll, "Hell Let Loose", HLL, 26420);
game_query_mod!(imic, "Insurgency: Modern Infantry Combat", IMIC, 27015);
game_query_mod!(insurgency, "Insurgency", INSURGENCY, 27015);
game_query_mod!(
insurgencysandstorm,
"Insurgency: Sandstorm",
INSURGENCYSANDSTORM,
27131
);
game_query_mod!(left4dead, "Left 4 Dead", LEFT4DEAD, 27015);
game_query_mod!(left4dead2, "Left 4 Dead 2", LEFT4DEAD2, 27015);
game_query_mod!(ohd, "Operation: Harsh Doorstop", OHD, 27005);
game_query_mod!(onset, "Onset", ONSET, 7776);
game_query_mod!(projectzomboid, "Project Zomboid", PROJECTZOMBOID, 16261);
game_query_mod!(ror2, "Risk of Rain 2", ROR2, 27016);
game_query_mod!(rust, "Rust", RUST, 27015);
game_query_mod!(sco, "Sven Co-op", SCO, 27015);
game_query_mod!(sd2d, "7 Days To Die", SD2D, 26900);
game_query_mod!(teamfortress2, "Team Fortress 2", TEAMFORTRESS2, 27015);
game_query_mod!(tfc, "Team Fortress Classic", TFC, 27015);
game_query_mod!(theforest, "The Forest", THEFOREST, 27016);
game_query_mod!(unturned, "Unturned", UNTURNED, 27015);
game_query_mod!(vrising, "V Rising", VRISING, 27016);

52
crates/lib/src/lib.rs Normal file
View file

@ -0,0 +1,52 @@
//! Game Server Query Library.
//!
//! # Usage example:
//!
//! ## For a specific game
//! ```
//! use gamedig::games::teamfortress2;
//!
//! let response = teamfortress2::query(&"127.0.0.1".parse().unwrap(), None); // None is the default port (which is 27015), could also be Some(27015)
//! match response { // Result type, must check what it is...
//! Err(error) => println!("Couldn't query, error: {}", error),
//! Ok(r) => println!("{:#?}", r)
//! }
//! ```
//!
//! ## Using a game definition
//! ```
//! use gamedig::games::{GAMES, query};
//!
//! let game = GAMES.get("teamfortress2").unwrap(); // Get a game definition, the full list can be found in src/games/mod.rs
//! let response = query(game, &"127.0.0.1".parse().unwrap(), None); // None will use the default port
//! match response {
//! Err(error) => println!("Couldn't query, error: {}", error),
//! Ok(r) => println!("{:#?}", r.as_json()),
//! }
//! ```
//!
//! # Crate features:
//! Enabled by default: `games`, `game_defs`, `services`
//!
//! `serde` - enables json serialization/deserialization for all response types.
//! <br> `games` - include games support. <br>
//! `services` - include services support. <br>
//! `game_defs` - Include game definitions for programmatic access (enabled by
//! default).
pub mod errors;
#[cfg(feature = "games")]
pub mod games;
pub mod protocols;
#[cfg(feature = "services")]
pub mod services;
mod buffer;
mod socket;
mod utils;
pub use errors::*;
#[cfg(feature = "games")]
pub use games::*;
#[cfg(feature = "services")]
pub use services::*;

View file

@ -0,0 +1,19 @@
use crate::{GDErrorKind, GDResult};
use std::collections::HashMap;
pub fn has_password(server_vars: &mut HashMap<String, String>) -> GDResult<bool> {
let password_value = server_vars
.remove("password")
.ok_or(GDErrorKind::PacketBad.context("Missing password (exists) field"))?
.to_lowercase();
if let Ok(has) = password_value.parse::<bool>() {
return Ok(has);
}
let as_numeral: u8 = password_value
.parse()
.map_err(|e| GDErrorKind::TypeParse.context(e))?;
Ok(as_numeral != 0)
}

View file

@ -0,0 +1,86 @@
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
pub(crate) mod common;
/// The implementations.
pub mod protocols;
pub use protocols::*;
/// Versions of the gamespy protocol
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum GameSpyVersion {
One,
Two,
Three,
}
/// Versioned response type
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum VersionedResponse<'a> {
One(&'a one::Response),
Two(&'a two::Response),
Three(&'a three::Response),
}
/// Versioned player type
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum VersionedPlayer<'a> {
One(&'a one::Player),
Two(&'a two::Player),
Three(&'a three::Player),
}
/// Generate a module containing a query function for a gamespy game.
///
/// * `mod_name` - The name to be given to the game module (see ID naming
/// conventions in CONTRIBUTING.md).
/// * `pretty_name` - The full name of the game, will be used as the
/// documentation for the created module.
/// * `gamespy_ver`, `default_port` - Passed through to [game_query_fn].
macro_rules! game_query_mod {
($mod_name: ident, $pretty_name: expr, $gamespy_ver: ident, $default_port: literal) => {
#[doc = $pretty_name]
pub mod $mod_name {
crate::protocols::gamespy::game_query_fn!($gamespy_ver, $default_port);
}
};
}
pub(crate) use game_query_mod;
// Allow generating doc comments:
// https://users.rust-lang.org/t/macros-filling-text-in-comments/20473
/// Generate a query function for a gamespy game.
///
/// * `gamespy_ver` - The name of the [module](crate::protocols::gamespy) for
/// the gamespy version the game uses.
/// * `default_port` - The default port the game uses.
///
/// ```rust,ignore
/// use crate::protocols::gamespy::game_query_fn;
/// game_query_fn!(one, 7778);
/// ```
macro_rules! game_query_fn {
($gamespy_ver: ident, $default_port: literal) => {
crate::protocols::gamespy::game_query_fn! {@gen $gamespy_ver, $default_port, concat!(
"Make a gamespy ", stringify!($gamespy_ver), " query with default timeout settings.\n\n",
"If port is `None`, then the default port (", stringify!($default_port), ") will be used.")}
};
(@gen $gamespy_ver: ident, $default_port: literal, $doc: expr) => {
#[doc = $doc]
pub fn query(
address: &std::net::IpAddr,
port: Option<u16>,
) -> crate::GDResult<crate::protocols::gamespy::$gamespy_ver::Response> {
crate::protocols::gamespy::$gamespy_ver::query(
&std::net::SocketAddr::new(*address, port.unwrap_or($default_port)),
None,
)
}
};
}
pub(crate) use game_query_fn;

View file

@ -0,0 +1,3 @@
pub mod one;
pub mod three;
pub mod two;

View file

@ -0,0 +1,5 @@
pub mod protocol;
pub mod types;
pub use protocol::*;
pub use types::*;

View file

@ -0,0 +1,244 @@
use byteorder::LittleEndian;
use crate::buffer::Utf8Decoder;
use crate::protocols::gamespy::common::has_password;
use crate::GDErrorKind::TypeParse;
use crate::utils::retry_on_timeout;
use crate::{
buffer::Buffer,
protocols::{
gamespy::one::{Player, Response},
types::TimeoutSettings,
},
socket::{Socket, UdpSocket},
GDErrorKind,
GDResult,
};
use std::collections::HashMap;
use std::net::SocketAddr;
/// Send status request, and parse response into HashMap.
/// This function will retry fetch on timeouts.
fn get_server_values(
address: &SocketAddr,
timeout_settings: &Option<TimeoutSettings>,
) -> GDResult<HashMap<String, String>> {
let mut socket = UdpSocket::new(address)?;
socket.apply_timeout(timeout_settings)?;
retry_on_timeout(
TimeoutSettings::get_retries_or_default(timeout_settings),
move || get_server_values_impl(&mut socket),
)
}
/// Send status request, and parse response into HashMap (without retry logic).
fn get_server_values_impl(socket: &mut UdpSocket) -> GDResult<HashMap<String, String>> {
socket.send(b"\\status\\xserverquery")?;
let mut received_query_id: Option<usize> = None;
let mut parts: Vec<usize> = Vec::new();
let mut is_finished = false;
let mut server_values = HashMap::new();
while !is_finished {
let data = socket.receive(None)?;
let mut bufferer = Buffer::<LittleEndian>::new(&data);
let mut as_string = bufferer.read_string::<Utf8Decoder>(None)?;
as_string.remove(0);
let splited: Vec<String> = as_string.split('\\').map(str::to_string).collect();
for i in 0 .. splited.len() / 2 {
let position = i * 2;
let key = splited[position].clone();
let value = splited
.get(position + 1)
.map_or_else(String::new, |v| v.clone());
server_values.insert(key, value);
}
is_finished = server_values.remove("final").is_some();
let query_data = server_values.get("queryid");
let mut part = parts.len(); // if the part number isn't provided, it's value is the parts length
let mut query_id = None;
if let Some(qid) = query_data {
let split: Vec<&str> = qid.split('.').collect();
query_id = Some(split[0].parse().map_err(|e| TypeParse.context(e))?);
match split.len() {
1 => (),
2 => part = split[1].parse().map_err(|e| TypeParse.context(e))?,
_ => Err(GDErrorKind::PacketBad)?, /* the queryid can't be splitted in more than 2
* elements */
};
}
server_values.remove("queryid");
if received_query_id.is_some() && received_query_id != query_id {
return Err(GDErrorKind::PacketBad.into()); // wrong query id!
}
received_query_id = query_id;
match parts.contains(&part) {
true => Err(GDErrorKind::PacketBad)?,
false => parts.push(part),
}
}
Ok(server_values)
}
fn extract_players(server_vars: &mut HashMap<String, String>, players_maximum: u32) -> GDResult<Vec<Player>> {
let mut players_data: Vec<HashMap<String, String>> = Vec::with_capacity(players_maximum as usize);
server_vars.retain(|key, value| {
let split: Vec<&str> = key.split('_').collect();
if split.len() != 2 {
return true;
}
let kind = split[0];
let id: usize = match split[1].parse() {
Ok(v) => v,
Err(_) => return true,
};
let early_return = match kind {
"team" | "player" | "playername" | "ping" | "face" | "skin" | "mesh" | "frags" | "ngsecret" | "deaths"
| "health" => false,
_x => true, // println!("UNKNOWN {id} {x} {value}");
};
if early_return {
return true;
}
if id >= players_data.len() {
let others = vec![HashMap::new(); id - players_data.len() + 1];
players_data.extend_from_slice(&others);
}
players_data[id].insert(kind.to_string(), value.to_string());
false
});
let mut players: Vec<Player> = Vec::with_capacity(players_data.len());
for player_data in players_data {
let new_player = Player {
name: match player_data.get("player") {
Some(v) => v.clone(),
None => {
player_data
.get("playername")
.ok_or(GDErrorKind::PacketBad)?
.clone()
}
},
team: match player_data.get("team") {
Some(t) => Some(t.trim().parse().map_err(|e| TypeParse.context(e))?),
None => None,
},
ping: player_data
.get("ping")
.ok_or(GDErrorKind::PacketBad)?
.trim()
.parse()
.map_err(|e| TypeParse.context(e))?,
face: player_data.get("face").cloned(),
skin: player_data.get("skin").cloned(),
mesh: player_data.get("mesh").cloned(),
score: player_data
.get("frags")
.ok_or(GDErrorKind::PacketBad)?
.trim()
.parse()
.map_err(|e| TypeParse.context(e))?,
deaths: match player_data.get("deaths") {
Some(v) => Some(v.trim().parse().map_err(|e| TypeParse.context(e))?),
None => None,
},
health: match player_data.get("health") {
Some(v) => Some(v.trim().parse().map_err(|e| TypeParse.context(e))?),
None => None,
},
secret: match player_data.get("ngsecret") {
Some(s) => Some(s.to_lowercase().parse().map_err(|e| TypeParse.context(e))?),
None => None,
},
};
players.push(new_player);
}
Ok(players)
}
/// If there are parsing problems using the `query` function, you can directly
/// get the server's values using this function.
pub fn query_vars(
address: &SocketAddr,
timeout_settings: Option<TimeoutSettings>,
) -> GDResult<HashMap<String, String>> {
get_server_values(address, &timeout_settings)
}
/// Query a server by providing the address, the port and timeout settings.
/// Providing None to the timeout settings results in using the default values.
/// (TimeoutSettings::[default](TimeoutSettings::default)).
pub fn query(address: &SocketAddr, timeout_settings: Option<TimeoutSettings>) -> GDResult<Response> {
let mut server_vars = query_vars(address, timeout_settings)?;
let players_maximum: u32 = server_vars
.remove("maxplayers")
.ok_or(GDErrorKind::PacketBad)?
.parse()
.map_err(|e| TypeParse.context(e))?;
let players_minimum = match server_vars.remove("minplayers") {
None => None,
Some(v) => Some(v.parse::<u8>().map_err(|e| TypeParse.context(e))?),
};
let players = extract_players(&mut server_vars, players_maximum)?;
Ok(Response {
name: server_vars
.remove("hostname")
.ok_or(GDErrorKind::PacketBad)?,
map: server_vars
.remove("mapname")
.ok_or(GDErrorKind::PacketBad)?,
map_title: server_vars.remove("maptitle"),
admin_contact: server_vars.remove("AdminEMail"),
admin_name: server_vars
.remove("AdminName")
.or_else(|| server_vars.remove("admin")),
has_password: has_password(&mut server_vars)?,
game_mode: server_vars
.remove("gametype")
.ok_or(GDErrorKind::PacketBad)?,
game_version: server_vars
.remove("gamever")
.ok_or(GDErrorKind::PacketBad)?,
players_maximum,
players_online: players.len() as u32,
players_minimum,
players,
tournament: server_vars
.remove("tournament")
.unwrap_or_else(|| "true".to_string())
.to_lowercase()
.parse()
.map_err(|e| TypeParse.context(e))?,
unused_entries: server_vars,
})
}

View file

@ -0,0 +1,73 @@
use std::collections::HashMap;
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
use crate::protocols::gamespy::{VersionedPlayer, VersionedResponse};
use crate::protocols::types::{CommonPlayer, CommonResponse, GenericPlayer};
use crate::protocols::GenericResponse;
/// A players details.
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct Player {
pub name: String,
pub team: Option<u8>,
/// The ping from the server's perspective.
pub ping: u16,
pub face: Option<String>,
pub skin: Option<String>,
pub mesh: Option<String>,
pub score: i32,
pub deaths: Option<u32>,
pub health: Option<u32>,
pub secret: Option<bool>,
}
impl CommonPlayer for Player {
fn as_original(&self) -> GenericPlayer { GenericPlayer::Gamespy(VersionedPlayer::One(self)) }
fn name(&self) -> &str { &self.name }
fn score(&self) -> Option<i32> { Some(self.score) }
}
/// A query response.
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Response {
pub name: String,
pub map: String,
pub map_title: Option<String>,
pub admin_contact: Option<String>,
pub admin_name: Option<String>,
pub has_password: bool,
pub game_mode: String,
pub game_version: String,
pub players_maximum: u32,
pub players_online: u32,
pub players_minimum: Option<u8>,
pub players: Vec<Player>,
pub tournament: bool,
pub unused_entries: HashMap<String, String>,
}
impl CommonResponse for Response {
fn as_original(&self) -> GenericResponse { GenericResponse::GameSpy(VersionedResponse::One(self)) }
fn name(&self) -> Option<&str> { Some(&self.name) }
fn map(&self) -> Option<&str> { Some(&self.map) }
fn has_password(&self) -> Option<bool> { Some(self.has_password) }
fn game_mode(&self) -> Option<&str> { Some(&self.game_mode) }
fn game_version(&self) -> Option<&str> { Some(&self.game_version) }
fn players_maximum(&self) -> u32 { self.players_maximum }
fn players_online(&self) -> u32 { self.players_online }
fn players(&self) -> Option<Vec<&dyn CommonPlayer>> {
Some(
self.players
.iter()
.map(|p| p as &dyn CommonPlayer)
.collect(),
)
}
}

View file

@ -0,0 +1,5 @@
pub mod protocol;
pub mod types;
pub use protocol::*;
pub use types::*;

View file

@ -0,0 +1,412 @@
use byteorder::{BigEndian, LittleEndian};
use crate::buffer::{Buffer, Utf8Decoder};
use crate::protocols::gamespy::common::has_password;
use crate::protocols::gamespy::three::{Player, Response, Team};
use crate::protocols::types::TimeoutSettings;
use crate::socket::{Socket, UdpSocket};
use crate::utils::retry_on_timeout;
use crate::GDErrorKind::{PacketBad, TypeParse};
use crate::{GDErrorKind, GDResult};
use std::collections::HashMap;
use std::net::SocketAddr;
const THIS_SESSION_ID: u32 = 1;
struct RequestPacket {
header: u16,
kind: u8,
session_id: u32,
challenge: Option<i32>,
payload: Option<[u8; 4]>,
}
impl RequestPacket {
fn to_bytes(&self) -> Vec<u8> {
let mut packet: Vec<u8> = Vec::with_capacity(7);
packet.extend_from_slice(&self.header.to_be_bytes());
packet.push(self.kind);
packet.extend_from_slice(&self.session_id.to_be_bytes());
if let Some(challenge) = self.challenge {
packet.extend_from_slice(&challenge.to_be_bytes());
}
if let Some(payload) = self.payload {
packet.extend_from_slice(&payload);
}
packet
}
}
pub(crate) struct GameSpy3 {
socket: UdpSocket,
payload: [u8; 4],
single_packets: bool,
retry_count: usize,
}
const PACKET_SIZE: usize = 2048;
const DEFAULT_PAYLOAD: [u8; 4] = [0xFF, 0xFF, 0xFF, 0x01];
impl GameSpy3 {
fn new(address: &SocketAddr, timeout_settings: Option<TimeoutSettings>) -> GDResult<Self> {
let socket = UdpSocket::new(address)?;
let retry_count = TimeoutSettings::get_retries_or_default(&timeout_settings);
socket.apply_timeout(&timeout_settings)?;
Ok(Self {
socket,
payload: DEFAULT_PAYLOAD,
single_packets: false,
retry_count,
})
}
pub(crate) fn new_custom(
address: &SocketAddr,
timeout_settings: Option<TimeoutSettings>,
payload: [u8; 4],
single_packets: bool,
) -> GDResult<Self> {
let socket = UdpSocket::new(address)?;
let retry_count = TimeoutSettings::get_retries_or_default(&timeout_settings);
socket.apply_timeout(&timeout_settings)?;
Ok(Self {
socket,
payload,
single_packets,
retry_count,
})
}
fn receive(&mut self, size: Option<usize>, kind: u8) -> GDResult<Vec<u8>> {
let received = self.socket.receive(size.or(Some(PACKET_SIZE)))?;
let mut buf = Buffer::<BigEndian>::new(&received);
if buf.read::<u8>()? != kind {
return Err(PacketBad.context("Kind of packet did not match"));
}
if buf.read::<u32>()? != THIS_SESSION_ID {
return Err(PacketBad.context("Session ID did not match"));
}
Ok(buf.remaining_bytes().to_vec())
}
fn make_initial_handshake(&mut self) -> GDResult<Option<i32>> {
self.socket.send(
&RequestPacket {
header: 65277,
kind: 9,
session_id: THIS_SESSION_ID,
challenge: None,
payload: None,
}
.to_bytes(),
)?;
let data = self.receive(Some(16), 9)?;
let mut buf = Buffer::<LittleEndian>::new(&data);
let challenge_as_string = buf.read_string::<Utf8Decoder>(None)?;
let challenge = challenge_as_string
.parse()
.map_err(|e| TypeParse.context(e))?;
Ok(match challenge == 0 {
true => None,
false => Some(challenge),
})
}
fn send_data_request(&mut self, challenge: Option<i32>) -> GDResult<()> {
self.socket.send(
&RequestPacket {
header: 65277,
kind: 0,
session_id: THIS_SESSION_ID,
challenge,
payload: Some(self.payload),
}
.to_bytes(),
)
}
/// Fetch packets from server and store in buffer.
/// This function will retry fetch on timeouts.
pub(crate) fn get_server_packets(&mut self) -> GDResult<Vec<Vec<u8>>> {
retry_on_timeout(self.retry_count, move || self.get_server_packets_impl())
}
/// Fetch packets from server and store in buffer (without retry logic).
fn get_server_packets_impl(&mut self) -> GDResult<Vec<Vec<u8>>> {
let challenge = self.make_initial_handshake()?;
self.send_data_request(challenge)?;
let mut values: Vec<Vec<u8>> = Vec::new();
let mut reached_expected_packets_size = false;
while !reached_expected_packets_size {
let received_data = self.receive(None, 0)?;
let mut buf = Buffer::<BigEndian>::new(&received_data);
if self.single_packets {
buf.move_cursor(11)?;
return Ok(vec![buf.remaining_bytes().to_vec()]);
}
if buf.read_string::<Utf8Decoder>(None)? != "splitnum" {
return Err(PacketBad.context("Expected string \"splitnum\""));
}
let id = buf.read::<u8>()?;
let is_last = (id & 0x80) > 0;
let packet_id = (id & 0x7f) as usize;
buf.move_cursor(1)?; //unknown byte regarding packet no.
if is_last && packet_id + 1 != values.len() {
reached_expected_packets_size = true;
}
while values.len() <= packet_id {
values.push(Vec::new());
}
values[packet_id] = buf.remaining_bytes().to_vec();
}
if values.iter().any(Vec::is_empty) {
return Err(PacketBad.context("One (or more) packets is empty"));
}
Ok(values)
}
}
pub(crate) fn data_to_map(packet: &[u8]) -> GDResult<(HashMap<String, String>, Vec<u8>)> {
let mut vars = HashMap::new();
let mut buf = Buffer::<BigEndian>::new(packet);
while buf.remaining_length() != 0 {
let key = buf.read_string::<Utf8Decoder>(None)?;
if key.is_empty() {
break;
}
let value = buf.read_string::<Utf8Decoder>(None)?;
vars.insert(key, value);
}
Ok((vars, buf.remaining_bytes().to_vec()))
}
/// If there are parsing problems using the `query` function, you can directly
/// get the server's values using this function.
pub fn query_vars(
address: &SocketAddr,
timeout_settings: Option<TimeoutSettings>,
) -> GDResult<HashMap<String, String>> {
let mut client = GameSpy3::new(address, timeout_settings)?;
let packets = client.get_server_packets()?;
let mut vars = HashMap::new();
for packet in &packets {
let (key_values, _remaining_data) = data_to_map(packet)?;
vars.extend(key_values);
}
Ok(vars)
}
fn parse_players_and_teams(packets: Vec<Vec<u8>>) -> GDResult<(Vec<Player>, Vec<Team>)> {
let mut players_data: Vec<HashMap<String, String>> = vec![HashMap::new()];
let mut teams_data: Vec<HashMap<String, String>> = vec![HashMap::new()];
for packet in packets {
let mut buf = Buffer::<LittleEndian>::new(&packet);
while buf.remaining_length() != 0 {
if buf.read::<u8>()? < 3 {
continue;
}
buf.move_cursor(1)?;
let field = buf.read_string::<Utf8Decoder>(None)?;
if field.is_empty() {
continue;
}
let field_split: Vec<&str> = field.split('_').collect();
let field_name = field_split.first().ok_or(GDErrorKind::PacketBad)?;
if !["player", "score", "ping", "team", "deaths", "pid", "skill"].contains(field_name) {
continue;
}
let field_type = match field_split.get(1) {
None => None,
Some(v) => {
match v.is_empty() {
true => None,
false => {
if v != &"t" {
Err(GDErrorKind::PacketBad)?;
}
Some(v)
}
}
}
};
let mut offset = buf.read::<u8>()? as usize;
let data = match field_type.is_none() {
true => &mut players_data,
false => &mut teams_data,
};
while buf.remaining_length() != 0 {
let item = buf.read_string::<Utf8Decoder>(None)?;
if item.is_empty() {
break;
}
while data.len() <= offset {
data.push(HashMap::new());
}
let entry_data = data.get_mut(offset).ok_or(PacketBad)?;
entry_data.insert(field_name.to_string(), item);
offset += 1;
}
}
}
let mut players: Vec<Player> = Vec::new();
for player_data in players_data {
if player_data.is_empty() {
continue;
}
players.push(Player {
name: player_data.get("player").ok_or(PacketBad)?.to_string(),
score: player_data
.get("score")
.ok_or(GDErrorKind::PacketBad)?
.parse()
.map_err(|e| TypeParse.context(e))?,
ping: player_data
.get("ping")
.ok_or(GDErrorKind::PacketBad)?
.parse()
.map_err(|e| TypeParse.context(e))?,
team: player_data
.get("team")
.ok_or(GDErrorKind::PacketBad)?
.parse()
.map_err(|e| TypeParse.context(e))?,
deaths: player_data
.get("deaths")
.ok_or(GDErrorKind::PacketBad)?
.parse()
.map_err(|e| TypeParse.context(e))?,
skill: player_data
.get("skill")
.ok_or(GDErrorKind::PacketBad)?
.parse()
.map_err(|e| TypeParse.context(e))?,
});
}
let mut teams: Vec<Team> = Vec::new();
for team_data in teams_data {
if team_data.is_empty() {
continue;
}
teams.push(Team {
name: team_data
.get("team")
.ok_or(GDErrorKind::PacketBad)?
.to_string(),
score: team_data
.get("score")
.ok_or(GDErrorKind::PacketBad)?
.parse()
.map_err(|e| TypeParse.context(e))?,
});
}
Ok((players, teams))
}
/// Query a server by providing the address, the port and timeout settings.
/// Providing None to the timeout settings results in using the default values.
/// (TimeoutSettings::[default](TimeoutSettings::default)).
pub fn query(address: &SocketAddr, timeout_settings: Option<TimeoutSettings>) -> GDResult<Response> {
let mut client = GameSpy3::new(address, timeout_settings)?;
let packets = client.get_server_packets()?;
let (mut server_vars, remaining_data) = data_to_map(packets.get(0).ok_or(GDErrorKind::PacketBad)?)?;
let mut remaining_data_packets = vec![remaining_data];
remaining_data_packets.extend_from_slice(&packets[1 ..]);
let (players, teams) = parse_players_and_teams(remaining_data_packets)?;
let players_maximum = server_vars
.remove("maxplayers")
.ok_or(GDErrorKind::PacketBad)?
.parse()
.map_err(|e| TypeParse.context(e))?;
let players_minimum = match server_vars.remove("minplayers") {
None => None,
Some(v) => Some(v.parse::<u8>().map_err(|e| TypeParse.context(e))?),
};
let players_online: u32 = match server_vars.remove("numplayers") {
None => players.len(),
Some(v) => {
let reported_players = v.parse().map_err(|e| TypeParse.context(e))?;
match reported_players < players.len() {
true => players.len(),
false => reported_players,
}
}
} as u32;
Ok(Response {
name: server_vars
.remove("hostname")
.ok_or(GDErrorKind::PacketBad)?,
map: server_vars
.remove("mapname")
.ok_or(GDErrorKind::PacketBad)?,
has_password: has_password(&mut server_vars)?,
game_mode: server_vars
.remove("gametype")
.ok_or(GDErrorKind::PacketBad)?,
game_version: server_vars
.remove("gamever")
.ok_or(GDErrorKind::PacketBad)?,
players_maximum,
players_online,
players_minimum,
players,
teams,
tournament: server_vars
.remove("tournament")
.unwrap_or_else(|| "true".to_string())
.to_lowercase()
.parse()
.map_err(|e| TypeParse.context(e))?,
unused_entries: server_vars,
})
}

View file

@ -0,0 +1,75 @@
use crate::protocols::gamespy::{VersionedPlayer, VersionedResponse};
use crate::protocols::types::{CommonPlayer, CommonResponse, GenericPlayer};
use crate::protocols::GenericResponse;
use std::collections::HashMap;
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
/// A players details.
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct Player {
pub name: String,
pub score: i32,
pub ping: u16,
pub team: u8,
pub deaths: u32,
pub skill: u32,
}
impl CommonPlayer for Player {
fn as_original(&self) -> crate::protocols::types::GenericPlayer {
GenericPlayer::Gamespy(VersionedPlayer::Three(self))
}
fn name(&self) -> &str { &self.name }
fn score(&self) -> Option<i32> { Some(self.score) }
}
/// A team's details
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct Team {
pub name: String,
pub score: i32,
}
/// A query response.
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Response {
pub name: String,
pub map: String,
pub has_password: bool,
pub game_mode: String,
pub game_version: String,
pub players_maximum: u32,
pub players_online: u32,
pub players_minimum: Option<u8>,
pub players: Vec<Player>,
pub teams: Vec<Team>,
pub tournament: bool,
pub unused_entries: HashMap<String, String>,
}
impl CommonResponse for Response {
fn as_original(&self) -> GenericResponse { GenericResponse::GameSpy(VersionedResponse::Three(self)) }
fn name(&self) -> Option<&str> { Some(&self.name) }
fn map(&self) -> Option<&str> { Some(&self.map) }
fn has_password(&self) -> Option<bool> { Some(self.has_password) }
fn game_mode(&self) -> Option<&str> { Some(&self.game_mode) }
fn game_version(&self) -> Option<&str> { Some(&self.game_version) }
fn players_maximum(&self) -> u32 { self.players_maximum }
fn players_online(&self) -> u32 { self.players_online }
fn players(&self) -> Option<Vec<&dyn CommonPlayer>> {
Some(
self.players
.iter()
.map(|p| p as &dyn CommonPlayer)
.collect(),
)
}
}

View file

@ -0,0 +1,5 @@
pub mod protocol;
pub mod types;
pub use protocol::*;
pub use types::*;

View file

@ -0,0 +1,211 @@
use crate::buffer::{Buffer, Utf8Decoder};
use crate::protocols::gamespy::two::{Player, Response, Team};
use crate::protocols::types::TimeoutSettings;
use crate::socket::{Socket, UdpSocket};
use crate::utils::retry_on_timeout;
use crate::GDErrorKind::{PacketBad, TypeParse};
use crate::{GDErrorKind, GDResult};
use byteorder::BigEndian;
use std::collections::HashMap;
use std::net::SocketAddr;
struct GameSpy2 {
socket: UdpSocket,
retry_count: usize,
}
macro_rules! table_extract {
($table:expr, $name:literal, $index:expr) => {
$table
.get($name)
.ok_or(GDErrorKind::PacketBad)?
.get($index)
.ok_or(GDErrorKind::PacketBad)?
};
}
macro_rules! table_extract_parse {
($table:expr, $name:literal, $index:expr) => {
table_extract!($table, $name, $index)
.parse()
.map_err(|e| PacketBad.context(e))?
};
}
fn data_as_table(data: &mut Buffer<BigEndian>) -> GDResult<(HashMap<String, Vec<String>>, usize)> {
if data.read::<u8>()? != 0 {
Err(GDErrorKind::PacketBad)?;
}
let rows = data.read::<u8>()? as usize;
if rows == 0 {
return Ok((HashMap::new(), 0));
}
let mut column_heads = Vec::new();
let mut current_column = data.read_string::<Utf8Decoder>(None)?;
while !current_column.is_empty() {
column_heads.push(current_column);
current_column = data.read_string::<Utf8Decoder>(None)?;
}
let columns = column_heads.len();
let mut table = HashMap::with_capacity(columns);
for head in &column_heads {
// TODO: This doesn't look good nor it is performant, fix later
// By using &column_heads in the for loop instead of cloning column_heads, you
// avoid creating an unnecessary copy. However, column_heads is a
// Vec<String> and head is a &String (a reference to a string). Hence, to use
// head as a key to the HashMap, we still need to call clone(). This is because
// HashMap takes ownership of its keys and we cannot give it a reference to a
// local variable (head) that will be dropped at the end of the function.
table.insert(head.clone(), Vec::new());
}
for _ in 0 .. rows {
for column in &column_heads {
let value = data.read_string::<Utf8Decoder>(None)?;
table
.get_mut(column)
.ok_or(GDErrorKind::PacketBad)?
.push(value);
}
}
Ok((table, rows))
}
impl GameSpy2 {
fn new(address: &SocketAddr, timeout_settings: Option<TimeoutSettings>) -> GDResult<Self> {
let socket = UdpSocket::new(address)?;
let retry_count = TimeoutSettings::get_retries_or_default(&timeout_settings);
socket.apply_timeout(&timeout_settings)?;
Ok(Self {
socket,
retry_count,
})
}
/// Send fetch request to server and store result in buffer.
/// This function will retry fetch on timeouts.
fn request_data(&mut self) -> GDResult<(Vec<u8>, usize)> {
retry_on_timeout(self.retry_count, move || self.request_data_impl())
}
/// Send fetch request to server and store result in buffer (without retry
/// logic).
fn request_data_impl(&mut self) -> GDResult<(Vec<u8>, usize)> {
self.socket
.send(&[0xFE, 0xFD, 0x00, 0x00, 0x00, 0x00, 0x01, 0xFF, 0xFF, 0xFF])?;
let received = self.socket.receive(None)?;
let mut buf = Buffer::<BigEndian>::new(&received);
if buf.read::<u8>()? != 0 || buf.read::<u32>()? != 1 {
return Err(PacketBad.into());
}
let buf_index = buf.current_position();
Ok((received, buf_index))
}
}
fn get_server_vars(bufferer: &mut Buffer<BigEndian>) -> GDResult<HashMap<String, String>> {
let mut values = HashMap::new();
let mut done_processing_vars = false;
while !done_processing_vars && bufferer.remaining_length() != 0 {
let key = bufferer.read_string::<Utf8Decoder>(None)?;
let value = bufferer.read_string::<Utf8Decoder>(None)?;
if key.is_empty() {
if value.is_empty() {
bufferer.move_cursor(-1)?;
done_processing_vars = true;
}
continue;
}
values.insert(key, value);
}
Ok(values)
}
fn get_teams(bufferer: &mut Buffer<BigEndian>) -> GDResult<Vec<Team>> {
let mut teams = Vec::new();
let (table, entries) = data_as_table(bufferer)?;
for index in 0 .. entries {
teams.push(Team {
name: table_extract!(table, "team_t", index).clone(),
score: table_extract_parse!(table, "score_t", index),
});
}
Ok(teams)
}
fn get_players(bufferer: &mut Buffer<BigEndian>) -> GDResult<Vec<Player>> {
let mut players = Vec::new();
let (table, entries) = data_as_table(bufferer)?;
for index in 0 .. entries {
players.push(Player {
name: table_extract!(table, "player_", index).clone(),
score: table_extract_parse!(table, "score_", index),
ping: table_extract_parse!(table, "ping_", index),
team_index: table_extract_parse!(table, "team_", index),
});
}
Ok(players)
}
pub fn query(address: &SocketAddr, timeout_settings: Option<TimeoutSettings>) -> GDResult<Response> {
let mut client = GameSpy2::new(address, timeout_settings)?;
let (data, buf_index) = client.request_data()?;
let mut buffer = Buffer::<BigEndian>::new(&data);
buffer.move_cursor(buf_index as isize)?;
let mut server_vars = get_server_vars(&mut buffer)?;
let players = get_players(&mut buffer)?;
let players_online = match server_vars.remove("numplayers") {
None => players.len(),
Some(v) => {
let reported_players = v.parse().map_err(|e| TypeParse.context(e))?;
match reported_players < players.len() {
true => players.len(),
false => reported_players,
}
}
} as u32;
let players_minimum = match server_vars.remove("minplayers") {
None => None,
Some(v) => Some(v.parse::<u32>().map_err(|e| TypeParse.context(e))?),
};
Ok(Response {
name: server_vars.remove("hostname").ok_or(PacketBad)?,
map: server_vars.remove("mapname").ok_or(PacketBad)?,
has_password: server_vars.remove("password").ok_or(PacketBad)? == "1",
teams: get_teams(&mut buffer)?,
players_maximum: server_vars
.remove("maxplayers")
.ok_or(PacketBad)?
.parse()
.map_err(|e| TypeParse.context(e))?,
players_online,
players_minimum,
players,
unused_entries: server_vars,
})
}

View file

@ -0,0 +1,64 @@
use std::collections::HashMap;
use crate::protocols::gamespy::{VersionedPlayer, VersionedResponse};
use crate::protocols::types::{CommonPlayer, CommonResponse, GenericPlayer};
use crate::protocols::GenericResponse;
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct Team {
pub name: String,
pub score: u16,
}
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct Player {
pub name: String,
pub score: u16,
pub ping: u16,
pub team_index: u16,
}
impl CommonPlayer for Player {
fn as_original(&self) -> GenericPlayer { GenericPlayer::Gamespy(VersionedPlayer::Two(self)) }
fn name(&self) -> &str { &self.name }
fn score(&self) -> Option<i32> { Some(self.score.into()) }
}
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Response {
pub name: String,
pub map: String,
pub has_password: bool,
pub teams: Vec<Team>,
pub players_maximum: u32,
pub players_online: u32,
pub players_minimum: Option<u32>,
pub players: Vec<Player>,
pub unused_entries: HashMap<String, String>,
}
impl CommonResponse for Response {
fn as_original(&self) -> GenericResponse { GenericResponse::GameSpy(VersionedResponse::Two(self)) }
fn name(&self) -> Option<&str> { Some(&self.name) }
fn map(&self) -> Option<&str> { Some(&self.map) }
fn has_password(&self) -> Option<bool> { Some(self.has_password) }
fn players_maximum(&self) -> u32 { self.players_maximum }
fn players_online(&self) -> u32 { self.players_online }
fn players(&self) -> Option<Vec<&dyn CommonPlayer>> {
Some(
self.players
.iter()
.map(|p| p as &dyn CommonPlayer)
.collect(),
)
}
}

View file

@ -0,0 +1,8 @@
/// The implementation.
pub mod protocol;
/// All types used by the implementation.
pub mod types;
pub use protocol::*;
pub use protocol::*;
pub use types::*;

View file

@ -0,0 +1,114 @@
// This file has code that has been documented by the NodeJS GameDig library
// (MIT) from https://github.com/gamedig/node-gamedig/blob/master/protocols/minecraftbedrock.js
use crate::{
buffer::{Buffer, Utf8Decoder},
protocols::{
minecraft::{BedrockResponse, GameMode, Server},
types::TimeoutSettings,
},
socket::{Socket, UdpSocket},
utils::{error_by_expected_size, retry_on_timeout},
GDErrorKind::{PacketBad, TypeParse},
GDResult,
};
use std::net::SocketAddr;
use byteorder::LittleEndian;
pub struct Bedrock {
socket: UdpSocket,
retry_count: usize,
}
impl Bedrock {
fn new(address: &SocketAddr, timeout_settings: Option<TimeoutSettings>) -> GDResult<Self> {
let socket = UdpSocket::new(address)?;
socket.apply_timeout(&timeout_settings)?;
let retry_count = TimeoutSettings::get_retries_or_default(&timeout_settings);
Ok(Self {
socket,
retry_count,
})
}
fn send_status_request(&mut self) -> GDResult<()> {
self.socket.send(&[
0x01, // Message ID: ID_UNCONNECTED_PING
0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, // Nonce / timestamp
0x00, 0xff, 0xff, 0x00, 0xfe, 0xfe, 0xfe, 0xfe, 0xfd, 0xfd, 0xfd, 0xfd, 0x12, 0x34, // Magic
0x56, 0x78, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Client GUID
])?;
Ok(())
}
/// Send a status request, and parse the response.
/// This function will retry fetch on timeouts.
fn get_info(&mut self) -> GDResult<BedrockResponse> {
retry_on_timeout(self.retry_count, move || self.get_info_impl())
}
/// Send a status request, and parse the response (without retry logic).
fn get_info_impl(&mut self) -> GDResult<BedrockResponse> {
self.send_status_request()?;
let received = self.socket.receive(None)?;
let mut buffer = Buffer::<LittleEndian>::new(&received);
if buffer.read::<u8>()? != 0x1c {
return Err(PacketBad.context("Expected 0x1c"));
}
// Checking for our nonce directly from a u64 (as the nonce is 8 bytes).
if buffer.read::<u64>()? != 9_833_440_827_789_222_417 {
return Err(PacketBad.context("Invalid nonce"));
}
// These 8 bytes are identical to the serverId string we receive in decimal
// below
buffer.move_cursor(8)?;
// Verifying the magic value (as we need 16 bytes, cast to two u64 values)
if buffer.read::<u64>()? != 18_374_403_896_610_127_616 {
return Err(PacketBad.context("Invalid magic"));
}
if buffer.read::<u64>()? != 8_671_175_388_723_805_693 {
return Err(PacketBad.context("Invalid magic"));
}
let remaining_length = buffer.switch_endian_chunk(2)?.read::<u16>()? as usize;
error_by_expected_size(remaining_length, buffer.remaining_length())?;
let binding = buffer.read_string::<Utf8Decoder>(None)?;
let status: Vec<&str> = binding.split(';').collect();
// We must have at least 6 values
if status.len() < 6 {
return Err(PacketBad.context("Not enough values"));
}
Ok(BedrockResponse {
edition: status[0].to_string(),
name: status[1].to_string(),
version_name: status[3].to_string(),
protocol_version: status[2].to_string(),
players_maximum: status[5].parse().map_err(|e| TypeParse.context(e))?,
players_online: status[4].parse().map_err(|e| TypeParse.context(e))?,
id: status.get(6).map(std::string::ToString::to_string),
map: status.get(7).map(std::string::ToString::to_string),
game_mode: match status.get(8) {
None => None,
Some(v) => Some(GameMode::from_bedrock(v)?),
},
server_type: Server::Bedrock,
})
}
pub fn query(address: &SocketAddr, timeout_settings: Option<TimeoutSettings>) -> GDResult<BedrockResponse> {
Self::new(address, timeout_settings)?.get_info()
}
}

View file

@ -0,0 +1,177 @@
use crate::{
buffer::Buffer,
protocols::{
minecraft::{as_varint, get_string, get_varint, JavaResponse, Player, Server},
types::TimeoutSettings,
},
socket::{Socket, TcpSocket},
utils::retry_on_timeout,
GDErrorKind::{JsonParse, PacketBad},
GDResult,
};
use std::net::SocketAddr;
use crate::protocols::minecraft::{as_string, RequestSettings};
use byteorder::LittleEndian;
use serde_json::Value;
pub struct Java {
socket: TcpSocket,
request_settings: RequestSettings,
retry_count: usize,
}
impl Java {
fn new(
address: &SocketAddr,
timeout_settings: Option<TimeoutSettings>,
request_settings: Option<RequestSettings>,
) -> GDResult<Self> {
let socket = TcpSocket::new(address)?;
socket.apply_timeout(&timeout_settings)?;
let retry_count = TimeoutSettings::get_retries_or_default(&timeout_settings);
Ok(Self {
socket,
request_settings: request_settings.unwrap_or_default(),
retry_count,
})
}
fn send(&mut self, data: Vec<u8>) -> GDResult<()> {
self.socket
.send(&[as_varint(data.len() as i32), data].concat())
}
fn receive(&mut self) -> GDResult<Vec<u8>> {
let data = &self.socket.receive(None)?;
let mut buffer = Buffer::<LittleEndian>::new(data);
let _packet_length = get_varint(&mut buffer)? as usize;
// this declared 'packet length' from within the packet might be wrong (?), not
// checking with it...
Ok(buffer.remaining_bytes().to_vec())
}
fn send_handshake(&mut self) -> GDResult<()> {
let handshake_payload = [
&[
// Packet ID (0)
0x00,
], // Protocol Version (-1 to determine version)
as_varint(self.request_settings.protocol_version).as_slice(),
// Server address (can be anything)
as_string(&self.request_settings.hostname)?.as_slice(),
// Server port (can be anything)
&self.socket.port().to_le_bytes(),
&[
// Next state (1 for status)
0x01,
],
]
.concat();
self.send(handshake_payload)?;
Ok(())
}
fn send_status_request(&mut self) -> GDResult<()> {
self.send(
[0x00] // Packet ID (0)
.to_vec(),
)?;
Ok(())
}
fn send_ping_request(&mut self) -> GDResult<()> {
self.send(
[0x01] // Packet ID (1)
.to_vec(),
)?;
Ok(())
}
/// Send minecraft ping request and parse the response.
/// This function will retry fetch on timeouts.
fn get_info(&mut self) -> GDResult<JavaResponse> {
retry_on_timeout(self.retry_count, move || self.get_info_impl())
}
/// Send minecraft ping request and parse the response (without retry
/// logic).
fn get_info_impl(&mut self) -> GDResult<JavaResponse> {
self.send_handshake()?;
self.send_status_request()?;
self.send_ping_request()?;
let socket_data = self.receive()?;
let mut buffer = Buffer::<LittleEndian>::new(&socket_data);
if get_varint(&mut buffer)? != 0 {
// first var int is the packet id
return Err(PacketBad.context("Expected 0"));
}
let json_response = get_string(&mut buffer)?;
let value_response: Value = serde_json::from_str(&json_response).map_err(|e| JsonParse.context(e))?;
let game_version = value_response["version"]["name"]
.as_str()
.ok_or(PacketBad)?
.to_string();
let protocol_version = value_response["version"]["protocol"]
.as_i64()
.ok_or(PacketBad)? as i32;
let max_players = value_response["players"]["max"].as_u64().ok_or(PacketBad)? as u32;
let online_players = value_response["players"]["online"]
.as_u64()
.ok_or(PacketBad)? as u32;
let players: Option<Vec<Player>> = match value_response["players"]["sample"].is_null() {
true => None,
false => {
Some({
let players_values = value_response["players"]["sample"]
.as_array()
.ok_or(PacketBad)?;
let mut players = Vec::with_capacity(players_values.len());
for player in players_values {
players.push(Player {
name: player["name"].as_str().ok_or(PacketBad)?.to_string(),
id: player["id"].as_str().ok_or(PacketBad)?.to_string(),
});
}
players
})
}
};
Ok(JavaResponse {
game_version,
protocol_version,
players_maximum: max_players,
players_online: online_players,
players,
description: value_response["description"].to_string(),
favicon: value_response["favicon"].as_str().map(str::to_string),
previews_chat: value_response["previewsChat"].as_bool(),
enforces_secure_chat: value_response["enforcesSecureChat"].as_bool(),
server_type: Server::Java,
})
}
pub fn query(
address: &SocketAddr,
timeout_settings: Option<TimeoutSettings>,
request_settings: Option<RequestSettings>,
) -> GDResult<JavaResponse> {
Self::new(address, timeout_settings, request_settings)?.get_info()
}
}

View file

@ -0,0 +1,82 @@
use crate::{
buffer::{Buffer, Utf16Decoder},
protocols::{
minecraft::{JavaResponse, LegacyGroup, Server},
types::TimeoutSettings,
},
socket::{Socket, TcpSocket},
utils::{error_by_expected_size, retry_on_timeout},
GDErrorKind::{PacketBad, ProtocolFormat},
GDResult,
};
use std::net::SocketAddr;
use byteorder::BigEndian;
pub struct LegacyV1_3 {
socket: TcpSocket,
retry_count: usize,
}
impl LegacyV1_3 {
fn new(address: &SocketAddr, timeout_settings: Option<TimeoutSettings>) -> GDResult<Self> {
let socket = TcpSocket::new(address)?;
socket.apply_timeout(&timeout_settings)?;
let retry_count = TimeoutSettings::get_retries_or_default(&timeout_settings);
Ok(Self {
socket,
retry_count,
})
}
fn send_initial_request(&mut self) -> GDResult<()> { self.socket.send(&[0xFE]) }
/// Send request for info and parse response.
/// This function will retry fetch on timeouts.
fn get_info(&mut self) -> GDResult<JavaResponse> {
retry_on_timeout(self.retry_count, move || self.get_info_impl())
}
/// Send request for info and parse response (without retry logic).
fn get_info_impl(&mut self) -> GDResult<JavaResponse> {
self.send_initial_request()?;
let data = self.socket.receive(None)?;
let mut buffer = Buffer::<BigEndian>::new(&data);
if buffer.read::<u8>()? != 0xFF {
return Err(ProtocolFormat.context("Expected 0xFF"));
}
let length = buffer.read::<u16>()? * 2;
error_by_expected_size((length + 3) as usize, data.len())?;
let packet_string = buffer.read_string::<Utf16Decoder<BigEndian>>(None)?;
let split: Vec<&str> = packet_string.split('§').collect();
error_by_expected_size(3, split.len())?;
let description = split[0].to_string();
let online_players = split[1].parse().map_err(|e| PacketBad.context(e))?;
let max_players = split[2].parse().map_err(|e| PacketBad.context(e))?;
Ok(JavaResponse {
game_version: "Beta 1.8+".to_string(),
protocol_version: -1,
players_maximum: max_players,
players_online: online_players,
players: None,
description,
favicon: None,
previews_chat: None,
enforces_secure_chat: None,
server_type: Server::Legacy(LegacyGroup::V1_3),
})
}
pub fn query(address: &SocketAddr, timeout_settings: Option<TimeoutSettings>) -> GDResult<JavaResponse> {
Self::new(address, timeout_settings)?.get_info()
}
}

View file

@ -0,0 +1,85 @@
use byteorder::BigEndian;
use crate::{
buffer::{Buffer, Utf16Decoder},
protocols::{
minecraft::{protocol::legacy_v1_6::LegacyV1_6, JavaResponse, LegacyGroup, Server},
types::TimeoutSettings,
},
socket::{Socket, TcpSocket},
utils::{error_by_expected_size, retry_on_timeout},
GDErrorKind::{PacketBad, ProtocolFormat},
GDResult,
};
use std::net::SocketAddr;
pub struct LegacyV1_5 {
socket: TcpSocket,
retry_count: usize,
}
impl LegacyV1_5 {
fn new(address: &SocketAddr, timeout_settings: Option<TimeoutSettings>) -> GDResult<Self> {
let socket = TcpSocket::new(address)?;
socket.apply_timeout(&timeout_settings)?;
let retry_count = TimeoutSettings::get_retries_or_default(&timeout_settings);
Ok(Self {
socket,
retry_count,
})
}
fn send_initial_request(&mut self) -> GDResult<()> { self.socket.send(&[0xFE, 0x01]) }
/// Send info request and parse response.
/// This function will retry fetch on timeouts.
fn get_info(&mut self) -> GDResult<JavaResponse> {
retry_on_timeout(self.retry_count, move || self.get_info_impl())
}
/// Send info request and parse response (without retry logic).
fn get_info_impl(&mut self) -> GDResult<JavaResponse> {
self.send_initial_request()?;
let data = self.socket.receive(None)?;
let mut buffer = Buffer::<BigEndian>::new(&data);
if buffer.read::<u8>()? != 0xFF {
return Err(ProtocolFormat.context("Expected 0xFF"));
}
let length = buffer.read::<u16>()? * 2;
error_by_expected_size((length + 3) as usize, data.len())?;
if LegacyV1_6::is_protocol(&mut buffer)? {
return LegacyV1_6::get_response(&mut buffer);
}
let packet_string = buffer.read_string::<Utf16Decoder<BigEndian>>(None)?;
let split: Vec<&str> = packet_string.split('§').collect();
error_by_expected_size(3, split.len())?;
let description = split[0].to_string();
let online_players = split[1].parse().map_err(|e| PacketBad.context(e))?;
let max_players = split[2].parse().map_err(|e| PacketBad.context(e))?;
Ok(JavaResponse {
game_version: "1.4+".to_string(),
protocol_version: -1,
players_maximum: max_players,
players_online: online_players,
players: None,
description,
favicon: None,
previews_chat: None,
enforces_secure_chat: None,
server_type: Server::Legacy(LegacyGroup::V1_5),
})
}
pub fn query(address: &SocketAddr, timeout_settings: Option<TimeoutSettings>) -> GDResult<JavaResponse> {
Self::new(address, timeout_settings)?.get_info()
}
}

View file

@ -0,0 +1,119 @@
use byteorder::BigEndian;
use crate::{
buffer::{Buffer, Utf16Decoder},
protocols::{
minecraft::{JavaResponse, LegacyGroup, Server},
types::TimeoutSettings,
},
socket::{Socket, TcpSocket},
utils::{error_by_expected_size, retry_on_timeout},
GDErrorKind::{PacketBad, ProtocolFormat},
GDResult,
};
use std::net::SocketAddr;
pub struct LegacyV1_6 {
socket: TcpSocket,
retry_count: usize,
}
impl LegacyV1_6 {
fn new(address: &SocketAddr, timeout_settings: Option<TimeoutSettings>) -> GDResult<Self> {
let socket = TcpSocket::new(address)?;
socket.apply_timeout(&timeout_settings)?;
let retry_count = TimeoutSettings::get_retries_or_default(&timeout_settings);
Ok(Self {
socket,
retry_count,
})
}
fn send_initial_request(&mut self) -> GDResult<()> {
self.socket.send(&[
0xfe, // Packet ID (FE)
0x01, // Ping payload (01)
0xfa, // Packet identifier for plugin message
0x00, 0x07, // Length of 'GameDig' string (7) as unsigned short
0x00, 0x47, 0x00, 0x61, 0x00, 0x6D, 0x00, 0x65, 0x00, 0x44, 0x00, 0x69, 0x00,
0x67, // 'GameDig' string as UTF-16BE
])?;
Ok(())
}
pub(crate) fn is_protocol(buffer: &mut Buffer<BigEndian>) -> GDResult<bool> {
let state = buffer
.remaining_bytes()
.starts_with(&[0x00, 0xA7, 0x00, 0x31, 0x00, 0x00]);
if state {
buffer.move_cursor(6)?;
}
Ok(state)
}
pub(crate) fn get_response(buffer: &mut Buffer<BigEndian>) -> GDResult<JavaResponse> {
// This is a specific order!
let protocol_version = buffer
.read_string::<Utf16Decoder<BigEndian>>(None)?
.parse()
.map_err(|e| PacketBad.context(e))?;
let game_version = buffer.read_string::<Utf16Decoder<BigEndian>>(None)?;
let description = buffer.read_string::<Utf16Decoder<BigEndian>>(None)?;
let online_players = buffer
.read_string::<Utf16Decoder<BigEndian>>(None)?
.parse()
.map_err(|e| PacketBad.context(e))?;
let max_players = buffer
.read_string::<Utf16Decoder<BigEndian>>(None)?
.parse()
.map_err(|e| PacketBad.context(e))?;
Ok(JavaResponse {
game_version,
protocol_version,
players_maximum: max_players,
players_online: online_players,
players: None,
description,
favicon: None,
previews_chat: None,
enforces_secure_chat: None,
server_type: Server::Legacy(LegacyGroup::V1_6),
})
}
/// Send info request and parse response.
/// This function will retry fetch on timeouts.
fn get_info(&mut self) -> GDResult<JavaResponse> {
retry_on_timeout(self.retry_count, move || self.get_info_impl())
}
/// Send info request and parse response (without retry logic).
fn get_info_impl(&mut self) -> GDResult<JavaResponse> {
self.send_initial_request()?;
let data = self.socket.receive(None)?;
let mut buffer = Buffer::<BigEndian>::new(&data);
if buffer.read::<u8>()? != 0xFF {
return Err(ProtocolFormat.context("Expected 0xFF"));
}
let length = buffer.read::<u16>()? * 2;
error_by_expected_size((length + 3) as usize, data.len())?;
if !Self::is_protocol(&mut buffer)? {
return Err(ProtocolFormat.context("Not legacy 1.6 protocol"));
}
Self::get_response(&mut buffer)
}
pub fn query(address: &SocketAddr, timeout_settings: Option<TimeoutSettings>) -> GDResult<JavaResponse> {
Self::new(address, timeout_settings)?.get_info()
}
}

View file

@ -0,0 +1,91 @@
use crate::protocols::minecraft::types::RequestSettings;
use crate::{
protocols::minecraft::{
protocol::{
bedrock::Bedrock,
java::Java,
legacy_v1_3::LegacyV1_3,
legacy_v1_5::LegacyV1_5,
legacy_v1_6::LegacyV1_6,
},
BedrockResponse,
JavaResponse,
LegacyGroup,
},
protocols::types::TimeoutSettings,
GDErrorKind::AutoQuery,
GDResult,
};
use std::net::SocketAddr;
mod bedrock;
mod java;
mod legacy_v1_3;
mod legacy_v1_5;
mod legacy_v1_6;
/// Queries a Minecraft server with all the protocol variants one by one (Java
/// -> Bedrock -> Legacy (1.6 -> 1.4 -> Beta 1.8)).
pub fn query(
address: &SocketAddr,
timeout_settings: Option<TimeoutSettings>,
request_settings: Option<RequestSettings>,
) -> GDResult<JavaResponse> {
if let Ok(response) = query_java(address, timeout_settings.clone(), request_settings) {
return Ok(response);
}
if let Ok(response) = query_bedrock(address, timeout_settings.clone()) {
return Ok(JavaResponse::from_bedrock_response(response));
}
if let Ok(response) = query_legacy(address, timeout_settings) {
return Ok(response);
}
Err(AutoQuery.into())
}
/// Query a Java Server.
pub fn query_java(
address: &SocketAddr,
timeout_settings: Option<TimeoutSettings>,
request_settings: Option<RequestSettings>,
) -> GDResult<JavaResponse> {
Java::query(address, timeout_settings, request_settings)
}
/// Query a (Java) Legacy Server (1.6 -> 1.4 -> Beta 1.8).
pub fn query_legacy(address: &SocketAddr, timeout_settings: Option<TimeoutSettings>) -> GDResult<JavaResponse> {
if let Ok(response) = query_legacy_specific(LegacyGroup::V1_6, address, timeout_settings.clone()) {
return Ok(response);
}
if let Ok(response) = query_legacy_specific(LegacyGroup::V1_5, address, timeout_settings.clone()) {
return Ok(response);
}
if let Ok(response) = query_legacy_specific(LegacyGroup::V1_3, address, timeout_settings) {
return Ok(response);
}
Err(AutoQuery.into())
}
/// Query a specific (Java) Legacy Server.
pub fn query_legacy_specific(
group: LegacyGroup,
address: &SocketAddr,
timeout_settings: Option<TimeoutSettings>,
) -> GDResult<JavaResponse> {
match group {
LegacyGroup::V1_6 => LegacyV1_6::query(address, timeout_settings),
LegacyGroup::V1_5 => LegacyV1_5::query(address, timeout_settings),
LegacyGroup::V1_3 => LegacyV1_3::query(address, timeout_settings),
}
}
/// Query a Bedrock Server.
pub fn query_bedrock(address: &SocketAddr, timeout_settings: Option<TimeoutSettings>) -> GDResult<BedrockResponse> {
Bedrock::query(address, timeout_settings)
}

View file

@ -0,0 +1,295 @@
// Although its a lightly modified version, this file contains code
// by Jaiden Bernard (2021-2022 - MIT) from
// https://github.com/thisjaiden/golden_apple/blob/master/src/lib.rs
use crate::{
buffer::Buffer,
protocols::{
types::{CommonPlayer, CommonResponse, ExtraRequestSettings, GenericPlayer},
GenericResponse,
},
GDErrorKind::{InvalidInput, PacketBad, UnknownEnumCast},
GDResult,
};
use byteorder::ByteOrder;
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
/// The type of Minecraft Server you want to query.
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum Server {
/// Java Edition.
Java,
/// Legacy Java.
Legacy(LegacyGroup),
/// Bedrock Edition.
Bedrock,
}
/// Legacy Java (Versions) Groups.
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum LegacyGroup {
/// 1.6
V1_6,
/// 1.4 - 1.5
V1_5,
/// Beta 1.8 - 1.3
V1_3,
}
/// Information about a player.
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct Player {
pub name: String,
pub id: String,
}
impl CommonPlayer for Player {
fn as_original(&self) -> GenericPlayer { GenericPlayer::Minecraft(self) }
fn name(&self) -> &str { &self.name }
}
/// Versioned response type
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum VersionedResponse<'a> {
Bedrock(&'a BedrockResponse),
Java(&'a JavaResponse),
}
/// A Java query response.
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct JavaResponse {
/// Version name, example: "1.19.2".
pub game_version: String,
/// Protocol version, example: 760 (for 1.19.1 or 1.19.2).
/// Note that for versions below 1.6 this field is always -1.
pub protocol_version: i32,
/// Number of server capacity.
pub players_maximum: u32,
/// Number of online players.
pub players_online: u32,
/// Some online players (can be missing).
pub players: Option<Vec<Player>>,
/// Server's description or MOTD.
pub description: String,
/// The favicon (can be missing).
pub favicon: Option<String>,
/// Tells if the chat preview is enabled (can be missing).
pub previews_chat: Option<bool>,
/// Tells if secure chat is enforced (can be missing).
pub enforces_secure_chat: Option<bool>,
/// Tell's the server type.
pub server_type: Server,
}
/// Java-only additional request settings.
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct RequestSettings {
/// Some Minecraft servers do not respond as expected if this
/// isn't a specific value, `mc.hypixel.net` is an example.
pub hostname: String,
/// Specifies the client [protocol version number](https://wiki.vg/Protocol_version_numbers),
/// `-1` means anything.
pub protocol_version: i32,
}
impl Default for RequestSettings {
/// `hostname`: "gamedig"
/// `protocol_version`: -1
fn default() -> Self {
Self {
hostname: "gamedig".to_string(),
protocol_version: -1,
}
}
}
impl RequestSettings {
/// Make a new *RequestSettings* with just the hostname, the protocol
/// version defaults to -1
pub fn new_just_hostname(hostname: String) -> Self {
Self {
hostname,
protocol_version: -1,
}
}
}
impl From<ExtraRequestSettings> for RequestSettings {
fn from(value: ExtraRequestSettings) -> Self {
let default = Self::default();
Self {
hostname: value.hostname.unwrap_or(default.hostname),
protocol_version: value.protocol_version.unwrap_or(default.protocol_version),
}
}
}
impl CommonResponse for JavaResponse {
fn as_original(&self) -> GenericResponse { GenericResponse::Minecraft(VersionedResponse::Java(self)) }
fn description(&self) -> Option<&str> { Some(&self.description) }
fn players_maximum(&self) -> u32 { self.players_maximum }
fn players_online(&self) -> u32 { self.players_online }
fn game_version(&self) -> Option<&str> { Some(&self.game_version) }
fn players(&self) -> Option<Vec<&dyn CommonPlayer>> {
self.players
.as_ref()
.map(|players| players.iter().map(|p| p as &dyn CommonPlayer).collect())
}
}
/// A Bedrock Edition query response.
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct BedrockResponse {
/// Server's edition.
pub edition: String,
/// Server's name.
pub name: String,
/// Version name, example: "1.19.40".
pub version_name: String,
/// Protocol version, example: 760 (for 1.19.2).
pub protocol_version: String,
/// Maximum number of players the server reports it can hold.
pub players_maximum: u32,
/// Number of players on the server.
pub players_online: u32,
/// Server id.
pub id: Option<String>,
/// Currently running map's name.
pub map: Option<String>,
/// Current game mode.
pub game_mode: Option<GameMode>,
/// Tells the server type.
pub server_type: Server,
}
impl CommonResponse for BedrockResponse {
fn as_original(&self) -> GenericResponse { GenericResponse::Minecraft(VersionedResponse::Bedrock(self)) }
fn name(&self) -> Option<&str> { Some(&self.name) }
fn map(&self) -> Option<&str> { self.map.as_deref() }
fn game_version(&self) -> Option<&str> { Some(&self.version_name) }
fn players_maximum(&self) -> u32 { self.players_maximum }
fn players_online(&self) -> u32 { self.players_online }
}
impl JavaResponse {
pub fn from_bedrock_response(response: BedrockResponse) -> Self {
Self {
game_version: response.version_name,
protocol_version: 0,
players_maximum: response.players_maximum,
players_online: response.players_online,
players: None,
description: response.name,
favicon: None,
previews_chat: None,
enforces_secure_chat: None,
server_type: Server::Bedrock,
}
}
}
/// A server's game mode (used only by Bedrock servers.
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum GameMode {
Survival,
Creative,
Hardcore,
Spectator,
Adventure,
}
impl GameMode {
pub fn from_bedrock(value: &&str) -> GDResult<Self> {
match *value {
"Survival" => Ok(Self::Survival),
"Creative" => Ok(Self::Creative),
"Hardcore" => Ok(Self::Hardcore),
"Spectator" => Ok(Self::Spectator),
"Adventure" => Ok(Self::Adventure),
_ => Err(UnknownEnumCast.context(format!("Unknown gamemode {value:?}"))),
}
}
}
pub(crate) fn get_varint<B: ByteOrder>(buffer: &mut Buffer<B>) -> GDResult<i32> {
let mut result = 0;
let msb: u8 = 0b1000_0000;
let mask: u8 = !msb;
for i in 0 .. 5 {
let current_byte = buffer.read::<u8>()?;
result |= ((current_byte & mask) as i32) << (7 * i);
// The 5th byte is only allowed to have the 4 smallest bits set
if i == 4 && (current_byte & 0xf0 != 0) {
return Err(PacketBad.context("Bad 5th byte"));
}
if (current_byte & msb) == 0 {
break;
}
}
Ok(result)
}
pub(crate) fn as_varint(value: i32) -> Vec<u8> {
let mut bytes = vec![];
let mut reading_value = value;
let msb: u8 = 0b1000_0000;
let mask: i32 = 0b0111_1111;
for _ in 0 .. 5 {
let tmp = (reading_value & mask) as u8;
reading_value &= !mask;
reading_value = reading_value.rotate_right(7);
if reading_value == 0 {
bytes.push(tmp);
break;
}
bytes.push(tmp | msb);
}
bytes
}
pub(crate) fn get_string<B: ByteOrder>(buffer: &mut Buffer<B>) -> GDResult<String> {
let length = get_varint(buffer)? as usize;
let mut text = Vec::with_capacity(length);
for _ in 0 .. length {
text.push(buffer.read::<u8>()?)
}
String::from_utf8(text).map_err(|e| PacketBad.context(e))
}
pub(crate) fn as_string(value: &str) -> GDResult<Vec<u8>> {
let length = value
.len()
.try_into()
.map_err(|e| InvalidInput.context(e))?;
let mut buf = as_varint(length);
buf.extend(value.as_bytes());
Ok(buf)
}

View file

@ -0,0 +1,18 @@
//! Protocols that are currently implemented.
//!
//! A protocol will be here if it supports multiple entries, if not, its
//! implementation will be in that specific needed place, a protocol can be
//! independently queried.
/// Reference: [node-GameDig](https://github.com/gamedig/node-gamedig/blob/master/protocols/gamespy1.js)
pub mod gamespy;
/// Reference: [Server List Ping](https://wiki.vg/Server_List_Ping)
pub mod minecraft;
/// Reference: [node-GameDig](https://github.com/gamedig/node-gamedig/blob/master/protocols/quake1.js)
pub mod quake;
/// General types that are used by all protocols.
pub mod types;
/// Reference: [Server Query](https://developer.valvesoftware.com/wiki/Server_queries)
pub mod valve;
pub use types::{ExtraRequestSettings, GenericResponse, Protocol};

View file

@ -0,0 +1,146 @@
use byteorder::LittleEndian;
use crate::buffer::{Buffer, Utf8Decoder};
use crate::protocols::quake::types::Response;
use crate::protocols::types::TimeoutSettings;
use crate::socket::{Socket, UdpSocket};
use crate::utils::retry_on_timeout;
use crate::GDErrorKind::{PacketBad, TypeParse};
use crate::{GDErrorKind, GDResult};
use std::collections::HashMap;
use std::net::SocketAddr;
use std::slice::Iter;
pub trait QuakeClient {
type Player;
fn get_send_header<'a>() -> &'a str;
fn get_response_header<'a>() -> &'a str;
fn parse_player_string(data: Iter<&str>) -> GDResult<Self::Player>;
}
/// Send request and return result buffer.
/// This function will retry fetch on timeouts.
fn get_data<Client: QuakeClient>(
address: &SocketAddr,
timeout_settings: &Option<TimeoutSettings>,
) -> GDResult<Vec<u8>> {
let mut socket = UdpSocket::new(address)?;
socket.apply_timeout(timeout_settings)?;
retry_on_timeout(
TimeoutSettings::get_retries_or_default(timeout_settings),
move || get_data_impl::<Client>(&mut socket),
)
}
/// Send request and return result buffer (without retry logic).
fn get_data_impl<Client: QuakeClient>(socket: &mut UdpSocket) -> GDResult<Vec<u8>> {
socket.send(
&[
&[0xFF, 0xFF, 0xFF, 0xFF],
Client::get_send_header().as_bytes(),
&[0x00],
]
.concat(),
)?;
let data = socket.receive(None)?;
let mut bufferer = Buffer::<LittleEndian>::new(&data);
if bufferer.read::<u32>()? != u32::MAX {
return Err(PacketBad.context("Expected 4294967295"));
}
let response_header = Client::get_response_header().as_bytes();
if !bufferer.remaining_bytes().starts_with(response_header) {
Err(GDErrorKind::PacketBad)?;
}
bufferer.move_cursor(response_header.len() as isize)?;
Ok(bufferer.remaining_bytes().to_vec())
}
fn get_server_values(bufferer: &mut Buffer<LittleEndian>) -> GDResult<HashMap<String, String>> {
let data = bufferer.read_string::<Utf8Decoder>(Some([0x0A]))?;
let mut data_split = data.split('\\').collect::<Vec<&str>>();
if let Some(first) = data_split.first() {
if first == &"" {
data_split.remove(0);
}
}
let values = data_split.chunks(2);
let mut vars: HashMap<String, String> = 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<Client: QuakeClient>(bufferer: &mut Buffer<LittleEndian>) -> GDResult<Vec<Client::Player>> {
let mut players: Vec<Client::Player> = Vec::new();
// this needs to be looked at again as theres no way to check if the buffer has
// a remaining null byte the original code was:
// while !bufferer.is_remaining_empty() && bufferer.remaining_data() != [0x00]
while !bufferer.remaining_length() == 0 {
let data = bufferer.read_string::<Utf8Decoder>(Some([0x0A]))?;
let data_split = data.split(' ').collect::<Vec<&str>>();
let data_iter = data_split.iter();
players.push(Client::parse_player_string(data_iter)?);
}
Ok(players)
}
pub fn client_query<Client: QuakeClient>(
address: &SocketAddr,
timeout_settings: Option<TimeoutSettings>,
) -> GDResult<Response<Client::Player>> {
let data = get_data::<Client>(address, &timeout_settings)?;
let mut bufferer = Buffer::<LittleEndian>::new(&data);
let mut server_vars = get_server_values(&mut bufferer)?;
let players = get_players::<Client>(&mut bufferer)?;
Ok(Response {
name: server_vars
.remove("hostname")
.or(server_vars.remove("sv_hostname"))
.ok_or(GDErrorKind::PacketBad)?,
map: server_vars
.remove("mapname")
.or(server_vars.remove("map"))
.ok_or(GDErrorKind::PacketBad)?,
players_online: players.len() as u8,
players_maximum: server_vars
.remove("maxclients")
.or(server_vars.remove("sv_maxclients"))
.ok_or(GDErrorKind::PacketBad)?
.parse()
.map_err(|e| TypeParse.context(e))?,
players,
game_version: server_vars
.remove("version")
.or(server_vars.remove("*version")),
unused_entries: server_vars,
})
}
pub fn remove_wrapping_quotes<'a>(string: &&'a str) -> &'a str {
match string.starts_with('\"') && string.ends_with('\"') {
false => string,
true => &string[1 .. string.len() - 1],
}
}

View file

@ -0,0 +1,74 @@
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
pub mod one;
pub mod three;
pub mod two;
/// All types used by the implementation.
pub mod types;
pub use types::*;
mod client;
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum QuakeVersion {
One,
Two,
Three,
}
/// Generate a module containing a query function for a quake game.
///
/// * `mod_name` - The name to be given to the game module (see ID naming
/// conventions in CONTRIBUTING.md).
/// * `pretty_name` - The full name of the game, will be used as the
/// documentation for the created module.
/// * `quake_ver`, `default_port` - Passed through to [game_query_fn].
macro_rules! game_query_mod {
($mod_name: ident, $pretty_name: expr, $quake_ver: ident, $default_port: literal) => {
#[doc = $pretty_name]
pub mod $mod_name {
crate::protocols::quake::game_query_fn!($quake_ver, $default_port);
}
};
}
pub(crate) use game_query_mod;
// Allow generating doc comments:
// https://users.rust-lang.org/t/macros-filling-text-in-comments/20473
/// Generate a query function for a quake game.
///
/// * `quake_ver` - The name of the [module](crate::protocols::quake) for the
/// quake version the game uses.
/// * `default_port` - The default port the game uses.
///
/// ```rust,ignore
/// use crate::protocols::quake::game_query_fn;
/// game_query_fn!(one, 27500);
/// ```
macro_rules! game_query_fn {
($quake_ver: ident, $default_port: literal) => {
use crate::protocols::quake::$quake_ver::Player;
crate::protocols::quake::game_query_fn! {@gen $quake_ver, Player, $default_port, concat!(
"Make a quake ", stringify!($quake_ver), " query with default timeout settings.\n\n",
"If port is `None`, then the default port (", stringify!($default_port), ") will be used.")}
};
(@gen $quake_ver: ident, $player_type: ty, $default_port: literal, $doc: expr) => {
#[doc = $doc]
pub fn query(
address: &std::net::IpAddr,
port: Option<u16>,
) -> crate::GDResult<crate::protocols::quake::Response<$player_type>> {
crate::protocols::quake::$quake_ver::query(
&std::net::SocketAddr::new(*address, port.unwrap_or($default_port)),
None,
)
}
};
}
pub(crate) use game_query_fn;

View file

@ -0,0 +1,87 @@
use crate::protocols::quake::client::{client_query, remove_wrapping_quotes, QuakeClient};
use crate::protocols::quake::Response;
use crate::protocols::types::{CommonPlayer, GenericPlayer, TimeoutSettings};
use crate::GDErrorKind::TypeParse;
use crate::{GDErrorKind, GDResult};
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
use std::net::SocketAddr;
use std::slice::Iter;
use super::QuakePlayerType;
/// 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: u16,
pub time: u16,
pub ping: u16,
pub name: String,
pub skin: String,
pub color_primary: u8,
pub color_secondary: u8,
}
impl QuakePlayerType for Player {
fn version(response: &Response<Self>) -> super::VersionedResponse { super::VersionedResponse::One(response) }
}
impl CommonPlayer for Player {
fn as_original(&self) -> GenericPlayer { GenericPlayer::QuakeOne(self) }
fn name(&self) -> &str { &self.name }
fn score(&self) -> Option<i32> { Some(self.score.into()) }
}
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 str { "n" }
fn parse_player_string(mut data: Iter<&str>) -> GDResult<Self::Player> {
Ok(Player {
id: match data.next() {
None => Err(GDErrorKind::PacketBad)?,
Some(v) => v.parse().map_err(|e| TypeParse.context(e))?,
},
score: match data.next() {
None => Err(GDErrorKind::PacketBad)?,
Some(v) => v.parse().map_err(|e| TypeParse.context(e))?,
},
time: match data.next() {
None => Err(GDErrorKind::PacketBad)?,
Some(v) => v.parse().map_err(|e| TypeParse.context(e))?,
},
ping: match data.next() {
None => Err(GDErrorKind::PacketBad)?,
Some(v) => v.parse().map_err(|e| TypeParse.context(e))?,
},
name: match data.next() {
None => Err(GDErrorKind::PacketBad)?,
Some(v) => remove_wrapping_quotes(v).to_string(),
},
skin: match data.next() {
None => Err(GDErrorKind::PacketBad)?,
Some(v) => remove_wrapping_quotes(v).to_string(),
},
color_primary: match data.next() {
None => Err(GDErrorKind::PacketBad)?,
Some(v) => v.parse().map_err(|e| TypeParse.context(e))?,
},
color_secondary: match data.next() {
None => Err(GDErrorKind::PacketBad)?,
Some(v) => v.parse().map_err(|e| TypeParse.context(e))?,
},
})
}
}
pub fn query(address: &SocketAddr, timeout_settings: Option<TimeoutSettings>) -> GDResult<Response<Player>> {
client_query::<QuakeOne>(address, timeout_settings)
}

View file

@ -0,0 +1,24 @@
use crate::protocols::quake::client::{client_query, QuakeClient};
use crate::protocols::quake::two::QuakeTwo;
use crate::protocols::quake::Response;
use crate::protocols::types::TimeoutSettings;
use crate::GDResult;
use std::net::SocketAddr;
use std::slice::Iter;
pub use crate::protocols::quake::two::Player;
struct QuakeThree;
impl QuakeClient for QuakeThree {
type Player = Player;
fn get_send_header<'a>() -> &'a str { "getstatus" }
fn get_response_header<'a>() -> &'a str { "statusResponse\n" }
fn parse_player_string(data: Iter<&str>) -> GDResult<Self::Player> { QuakeTwo::parse_player_string(data) }
}
pub fn query(address: &SocketAddr, timeout_settings: Option<TimeoutSettings>) -> GDResult<Response<Player>> {
client_query::<QuakeThree>(address, timeout_settings)
}

View file

@ -0,0 +1,67 @@
use crate::protocols::quake::client::{client_query, remove_wrapping_quotes, QuakeClient};
use crate::protocols::quake::one::QuakeOne;
use crate::protocols::quake::Response;
use crate::protocols::types::{CommonPlayer, GenericPlayer, TimeoutSettings};
use crate::GDErrorKind::TypeParse;
use crate::{GDErrorKind, GDResult};
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
use std::net::SocketAddr;
use std::slice::Iter;
use super::QuakePlayerType;
/// Quake 2 player data.
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct Player {
pub score: i32,
pub ping: u16,
pub name: String,
pub address: Option<String>,
}
impl QuakePlayerType for Player {
fn version(response: &Response<Self>) -> super::VersionedResponse {
super::VersionedResponse::TwoAndThree(response)
}
}
impl CommonPlayer for Player {
fn as_original(&self) -> GenericPlayer { GenericPlayer::QuakeTwo(self) }
fn name(&self) -> &str { &self.name }
fn score(&self) -> Option<i32> { Some(self.score) }
}
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 str { "print\n" }
fn parse_player_string(mut data: Iter<&str>) -> GDResult<Self::Player> {
Ok(Player {
score: match data.next() {
None => Err(GDErrorKind::PacketBad)?,
Some(v) => v.parse().map_err(|e| TypeParse.context(e))?,
},
ping: match data.next() {
None => Err(GDErrorKind::PacketBad)?,
Some(v) => v.parse().map_err(|e| TypeParse.context(e))?,
},
name: match data.next() {
None => Err(GDErrorKind::PacketBad)?,
Some(v) => remove_wrapping_quotes(v).to_string(),
},
address: data.next().map(|v| remove_wrapping_quotes(v).to_string()),
})
}
}
pub fn query(address: &SocketAddr, timeout_settings: Option<TimeoutSettings>) -> GDResult<Response<Player>> {
client_query::<QuakeTwo>(address, timeout_settings)
}

View file

@ -0,0 +1,58 @@
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use crate::protocols::{
types::{CommonPlayer, CommonResponse},
GenericResponse,
};
/// General server information's.
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Response<P> {
/// Name of the server.
pub name: String,
/// Map name.
pub map: String,
/// Current online players.
pub players: Vec<P>,
/// Number of players on the server.
pub players_online: u8,
/// Maximum number of players the server reports it can hold.
pub players_maximum: u8,
/// The server version.
pub game_version: Option<String>,
/// Other server entries that weren't used.
pub unused_entries: HashMap<String, String>,
}
pub trait QuakePlayerType: Sized + CommonPlayer {
fn version(response: &Response<Self>) -> VersionedResponse;
}
impl<P: QuakePlayerType> CommonResponse for Response<P> {
fn as_original(&self) -> GenericResponse { GenericResponse::Quake(P::version(self)) }
fn name(&self) -> Option<&str> { Some(&self.name) }
fn game_version(&self) -> Option<&str> { self.game_version.as_deref() }
fn map(&self) -> Option<&str> { Some(&self.map) }
fn players_maximum(&self) -> u32 { self.players_maximum.into() }
fn players_online(&self) -> u32 { self.players_online.into() }
fn players(&self) -> Option<Vec<&dyn CommonPlayer>> {
Some(
self.players
.iter()
.map(|p| p as &dyn CommonPlayer)
.collect(),
)
}
}
/// Versioned response type
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum VersionedResponse<'a> {
One(&'a Response<crate::protocols::quake::one::Player>),
TwoAndThree(&'a Response<crate::protocols::quake::two::Player>),
}

View file

@ -0,0 +1,365 @@
use crate::protocols::{gamespy, minecraft, quake, valve};
use crate::GDErrorKind::InvalidInput;
use crate::GDResult;
use std::time::Duration;
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
/// Enumeration of all custom protocols
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum ProprietaryProtocol {
TheShip,
FFOW,
JC2M,
}
/// Enumeration of all valid protocol types
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Protocol {
Gamespy(gamespy::GameSpyVersion),
Minecraft(Option<minecraft::types::Server>),
Quake(quake::QuakeVersion),
Valve(valve::SteamApp),
#[cfg(feature = "games")]
PROPRIETARY(ProprietaryProtocol),
}
/// All response types
#[derive(Debug, Clone, PartialEq)]
pub enum GenericResponse<'a> {
GameSpy(gamespy::VersionedResponse<'a>),
Minecraft(minecraft::VersionedResponse<'a>),
Quake(quake::VersionedResponse<'a>),
Valve(&'a valve::Response),
#[cfg(feature = "games")]
TheShip(&'a crate::games::theship::Response),
#[cfg(feature = "games")]
FFOW(&'a crate::games::ffow::Response),
#[cfg(feature = "games")]
JC2M(&'a crate::games::jc2m::Response),
}
/// All player types
#[derive(Debug, Clone, PartialEq)]
pub enum GenericPlayer<'a> {
Valve(&'a valve::ServerPlayer),
QuakeOne(&'a quake::one::Player),
QuakeTwo(&'a quake::two::Player),
Minecraft(&'a minecraft::Player),
Gamespy(gamespy::VersionedPlayer<'a>),
#[cfg(feature = "games")]
TheShip(&'a crate::games::theship::TheShipPlayer),
#[cfg(feature = "games")]
JCMP2(&'a crate::games::jc2m::Player),
}
pub trait CommonResponse {
/// Get the original response type
fn as_original(&self) -> GenericResponse;
/// Get a struct that can be stored as JSON (you don't need to override
/// this)
fn as_json(&self) -> CommonResponseJson {
CommonResponseJson {
name: self.name(),
description: self.description(),
game_mode: self.game_mode(),
game_version: self.game_version(),
has_password: self.has_password(),
map: self.map(),
players_maximum: self.players_maximum(),
players_online: self.players_online(),
players_bots: self.players_bots(),
players: self
.players()
.map(|players| players.iter().map(|p| p.as_json()).collect()),
}
}
/// The name of the server
fn name(&self) -> Option<&str> { None }
/// Description of the server
fn description(&self) -> Option<&str> { None }
/// Name of the current game or game mode
fn game_mode(&self) -> Option<&str> { None }
/// Version of the game being run on the server
fn game_version(&self) -> Option<&str> { None }
/// The current map name
fn map(&self) -> Option<&str> { None }
/// Maximum number of players allowed to connect
fn players_maximum(&self) -> u32;
/// Number of players currently connected
fn players_online(&self) -> u32;
/// Number of bots currently connected
fn players_bots(&self) -> Option<u32> { None }
/// Whether the server requires a password to join
fn has_password(&self) -> Option<bool> { None }
/// Currently connected players
fn players(&self) -> Option<Vec<&dyn CommonPlayer>> { None }
}
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct CommonResponseJson<'a> {
pub name: Option<&'a str>,
pub description: Option<&'a str>,
pub game_mode: Option<&'a str>,
pub game_version: Option<&'a str>,
pub map: Option<&'a str>,
pub players_maximum: u32,
pub players_online: u32,
pub players_bots: Option<u32>,
pub has_password: Option<bool>,
pub players: Option<Vec<CommonPlayerJson<'a>>>,
}
pub trait CommonPlayer {
/// Get the original player type
fn as_original(&self) -> GenericPlayer;
/// Get a struct that can be stored as JSON (you don't need to override
/// this)
fn as_json(&self) -> CommonPlayerJson {
CommonPlayerJson {
name: self.name(),
score: self.score(),
}
}
/// Player name
fn name(&self) -> &str;
/// Player score
fn score(&self) -> Option<i32> { None }
}
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct CommonPlayerJson<'a> {
pub name: &'a str,
pub score: Option<i32>,
}
/// Timeout settings for socket operations
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct TimeoutSettings {
read: Option<Duration>,
write: Option<Duration>,
retries: usize,
}
impl TimeoutSettings {
/// Construct new settings, passing None will block indefinitely.
/// Passing zero Duration throws GDErrorKind::[InvalidInput].
///
/// The retry count is the number of extra tries once the original request
/// fails, so a value of "0" will only make a single request, whereas
/// "1" will try the request again once if it fails.
/// The retry count is per-request so for multi-request queries (valve) if a
/// single part fails that part can be retried up to `retries` times.
pub fn new(read: Option<Duration>, write: Option<Duration>, retries: usize) -> GDResult<Self> {
if let Some(read_duration) = read {
if read_duration == Duration::new(0, 0) {
return Err(InvalidInput.context("Read duration must not be 0"));
}
}
if let Some(write_duration) = write {
if write_duration == Duration::new(0, 0) {
return Err(InvalidInput.context("Write duration must not be 0"));
}
}
Ok(Self {
read,
write,
retries,
})
}
/// Get the read timeout.
pub const fn get_read(&self) -> Option<Duration> { self.read }
/// Get the write timeout.
pub const fn get_write(&self) -> Option<Duration> { self.write }
/// Get number of retries
pub const fn get_retries(&self) -> usize { self.retries }
/// Get the number of retries if there are timeout settings else fall back
/// to the default
pub const fn get_retries_or_default(timeout_settings: &Option<TimeoutSettings>) -> usize {
if let Some(timeout_settings) = timeout_settings {
timeout_settings.get_retries()
} else {
TimeoutSettings::const_default().get_retries()
}
}
/// Get the read and write durations if there are timeout settings else fall
/// back to the defaults
pub const fn get_read_and_write_or_defaults(
timeout_settings: &Option<TimeoutSettings>,
) -> (Option<Duration>, Option<Duration>) {
if let Some(timeout_settings) = timeout_settings {
(timeout_settings.get_read(), timeout_settings.get_write())
} else {
let default = TimeoutSettings::const_default();
(default.get_read(), default.get_write())
}
}
/// Default values are 4 seconds for both read and write, no retries.
pub const fn const_default() -> Self {
Self {
read: Some(Duration::from_secs(4)),
write: Some(Duration::from_secs(4)),
retries: 0,
}
}
}
impl Default for TimeoutSettings {
/// Default values are 4 seconds for both read and write, no retries.
fn default() -> Self { Self::const_default() }
}
/// Generic extra request settings
///
/// Fields of this struct may not be used depending on which protocol
/// is selected, the individual fields link to the specific places
/// they will be used with additional documentation.
///
/// ## Examples
/// Create minecraft settings with builder:
/// ```
/// use gamedig::protocols::{minecraft, ExtraRequestSettings};
/// let mc_settings: minecraft::RequestSettings = ExtraRequestSettings::default().set_hostname("mc.hypixel.net".to_string()).into();
/// ```
///
/// Create valve settings with builder:
/// ```
/// use gamedig::protocols::{valve, ExtraRequestSettings};
/// let valve_settings: valve::GatheringSettings = ExtraRequestSettings::default().set_check_app_id(false).into();
/// ```
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Default)]
pub struct ExtraRequestSettings {
/// The server's hostname.
///
/// Used by:
/// - [minecraft::RequestSettings#structfield.hostname]
pub hostname: Option<String>,
/// The protocol version to use.
///
/// Used by:
/// - [minecraft::RequestSettings#structfield.protocol_version]
pub protocol_version: Option<i32>,
/// Whether to gather player information
///
/// Used by:
/// - [valve::GatheringSettings#structfield.players]
pub gather_players: Option<bool>,
/// Whether to gather rule information.
///
/// Used by:
/// - [valve::GatheringSettings#structfield.rules]
pub gather_rules: Option<bool>,
/// Whether to check if the App ID is valid.
///
/// Used by:
/// - [valve::GatheringSettings#structfield.check_app_id]
pub check_app_id: Option<bool>,
}
impl ExtraRequestSettings {
/// [Sets hostname](ExtraRequestSettings#structfield.hostname)
pub fn set_hostname(mut self, hostname: String) -> Self {
self.hostname = Some(hostname);
self
}
/// [Sets protocol
/// version](ExtraRequestSettings#structfield.protocol_version)
pub fn set_protocol_version(mut self, protocol_version: i32) -> Self {
self.protocol_version = Some(protocol_version);
self
}
/// [Sets gather players](ExtraRequestSettings#structfield.gather_players)
pub fn set_gather_players(mut self, gather_players: bool) -> Self {
self.gather_players = Some(gather_players);
self
}
/// [Sets gather rules](ExtraRequestSettings#structfield.gather_rules)
pub fn set_gather_rules(mut self, gather_rules: bool) -> Self {
self.gather_rules = Some(gather_rules);
self
}
/// [Sets check app ID](ExtraRequestSettings#structfield.check_app_id)
pub fn set_check_app_id(mut self, check_app_id: bool) -> Self {
self.check_app_id = Some(check_app_id);
self
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::time::Duration;
// Test creating new TimeoutSettings with valid durations
#[test]
fn test_new_with_valid_durations() -> GDResult<()> {
// Define valid read and write durations
let read_duration = Duration::from_secs(1);
let write_duration = Duration::from_secs(2);
// Create new TimeoutSettings with the valid durations
let timeout_settings = TimeoutSettings::new(Some(read_duration), Some(write_duration), 0)?;
// Verify that the get_read and get_write methods return the expected values
assert_eq!(timeout_settings.get_read(), Some(read_duration));
assert_eq!(timeout_settings.get_write(), Some(write_duration));
Ok(())
}
// Test creating new TimeoutSettings with a zero duration
#[test]
fn test_new_with_zero_duration() {
// Define a zero read duration and a valid write duration
let read_duration = Duration::new(0, 0);
let write_duration = Duration::from_secs(2);
// Try to create new TimeoutSettings with the zero read duration (this should
// fail)
let result = TimeoutSettings::new(Some(read_duration), Some(write_duration), 0);
// Verify that the function returned an error and that the error type is
// InvalidInput
assert!(result.is_err());
assert_eq!(result.unwrap_err(), crate::GDErrorKind::InvalidInput.into());
}
// Test that the default TimeoutSettings values are correct
#[test]
fn test_default_values() {
// Get the default TimeoutSettings values
let default_settings = TimeoutSettings::default();
// Verify that the get_read and get_write methods return the expected default
// values
assert_eq!(default_settings.get_read(), Some(Duration::from_secs(4)));
assert_eq!(default_settings.get_write(), Some(Duration::from_secs(4)));
}
// Test that extra request settings can be converted
#[test]
fn test_extra_request_settings() {
let settings = ExtraRequestSettings::default();
let _: minecraft::RequestSettings = settings.clone().into();
let _: valve::GatheringSettings = settings.into();
}
}

View file

@ -0,0 +1,60 @@
/// The implementation.
pub mod protocol;
/// All types used by the implementation.
pub mod types;
pub use protocol::*;
pub use types::*;
/// Generate a module containing a query function for a valve game.
///
/// * `mod_name` - The name to be given to the game module (see ID naming
/// conventions in CONTRIBUTING.md).
/// * `pretty_name` - The full name of the game, will be used as the
/// documentation for the created module.
/// * `steam_app`, `default_port` - Passed through to [game_query_fn].
macro_rules! game_query_mod {
($mod_name: ident, $pretty_name: expr, $steam_app: ident, $default_port: literal) => {
#[doc = $pretty_name]
pub mod $mod_name {
crate::protocols::valve::game_query_fn!($steam_app, $default_port);
}
};
}
pub(crate) use game_query_mod;
// Allow generating doc comments:
// https://users.rust-lang.org/t/macros-filling-text-in-comments/20473
/// Generate a query function for a valve game.
///
/// * `steam_app` - The entry in the [SteamApp] enum that the game uses.
/// * `default_port` - The default port the game uses.
///
/// ```rust,ignore
/// use crate::protocols::valve::game_query_fn;
/// game_query_fn!(TEAMFORTRESS2, 27015);
/// ```
macro_rules! game_query_fn {
($steam_app: ident, $default_port: literal) => {
crate::protocols::valve::game_query_fn!{@gen $steam_app, $default_port, concat!(
"Make a valve query for ", stringify!($steam_app), " with default timeout settings and default extra request settings.\n\n",
"If port is `None`, then the default port (", stringify!($default_port), ") will be used.")}
};
(@gen $steam_app: ident, $default_port: literal, $doc: expr) => {
#[doc = $doc]
pub fn query(address: &std::net::IpAddr, port: Option<u16>) -> crate::GDResult<crate::protocols::valve::game::Response> {
let valve_response = crate::protocols::valve::query(
&std::net::SocketAddr::new(*address, port.unwrap_or($default_port)),
crate::protocols::valve::SteamApp::$steam_app.as_engine(),
None,
None,
)?;
Ok(crate::protocols::valve::game::Response::new_from_valve_response(valve_response))
}
};
}
pub(crate) use game_query_fn;

View file

@ -0,0 +1,483 @@
use crate::{
buffer::Buffer,
protocols::{
types::TimeoutSettings,
valve::{
types::{
Environment,
ExtraData,
GatheringSettings,
Request,
Response,
Server,
ServerInfo,
ServerPlayer,
TheShip,
},
Engine,
ModData,
SteamApp,
},
},
socket::{Socket, UdpSocket},
utils::{retry_on_timeout, u8_lower_upper},
GDErrorKind::{BadGame, Decompress, UnknownEnumCast},
GDResult,
};
use bzip2_rs::decoder::Decoder;
use crate::buffer::Utf8Decoder;
use crate::protocols::valve::Packet;
use byteorder::LittleEndian;
use std::collections::HashMap;
use std::net::SocketAddr;
#[derive(Debug)]
#[allow(dead_code)] //remove this later on
struct SplitPacket {
pub header: u32,
pub id: u32,
pub total: u8,
pub number: u8,
pub size: u16,
/// None means its not compressed, Some means it is
/// and it contains (size and crc32)
pub decompressed: Option<(u32, u32)>,
payload: Vec<u8>,
}
impl SplitPacket {
fn new(engine: &Engine, protocol: u8, buffer: &mut Buffer<LittleEndian>) -> GDResult<Self> {
let header = buffer.read()?; //buffer.get_u32()?;
let id = buffer.read()?;
let (total, number, size, decompressed) = match engine {
Engine::GoldSrc(_) => {
let (lower, upper) = u8_lower_upper(buffer.read()?);
(lower, upper, 0, None)
}
Engine::Source(_) => {
let total = buffer.read()?;
let number = buffer.read()?;
let size = match protocol == 7 && (*engine == SteamApp::CSS.as_engine()) {
// certain apps with protocol = 7 dont have this field
false => buffer.read()?,
true => 1248,
};
let is_compressed = ((id >> 31) & 1u32) == 1u32;
let decompressed = match is_compressed {
false => None,
true => Some((buffer.read()?, buffer.read()?)),
};
(total, number, size, decompressed)
}
};
Ok(Self {
header,
id,
total,
number,
size,
decompressed,
payload: buffer.remaining_bytes().to_vec(),
})
}
fn get_payload(&self) -> GDResult<Vec<u8>> {
if let Some(decompressed) = self.decompressed {
let mut decoder = Decoder::new();
decoder
.write(&self.payload)
.map_err(|e| Decompress.context(e))?;
let decompressed_size = decompressed.0 as usize;
let mut decompressed_payload = vec![0; decompressed_size];
decoder
.read(&mut decompressed_payload)
.map_err(|e| Decompress.context(e))?;
if decompressed_payload.len() != decompressed_size
|| crc32fast::hash(&decompressed_payload) != decompressed.1
{
Err(Decompress.context(format!(
"Decompressed size {} was not expected {}",
decompressed_payload.len(),
decompressed_size
)))
} else {
Ok(decompressed_payload)
}
} else {
Ok(self.payload.clone())
}
}
}
pub(crate) struct ValveProtocol {
socket: UdpSocket,
retry_count: usize,
}
static PACKET_SIZE: usize = 6144;
impl ValveProtocol {
pub fn new(address: &SocketAddr, timeout_settings: Option<TimeoutSettings>) -> GDResult<Self> {
let socket = UdpSocket::new(address)?;
let retry_count = timeout_settings
.as_ref()
.map(|t| t.get_retries())
.unwrap_or_else(|| TimeoutSettings::default().get_retries());
socket.apply_timeout(&timeout_settings)?;
Ok(Self {
socket,
retry_count,
})
}
fn receive(&mut self, engine: &Engine, protocol: u8, buffer_size: usize) -> GDResult<Packet> {
let data = self.socket.receive(Some(buffer_size))?;
let mut buffer = Buffer::<LittleEndian>::new(&data);
let header: u8 = buffer.read()?;
buffer.move_cursor(-1)?;
if header == 0xFE {
// the packet is split
let mut main_packet = SplitPacket::new(engine, protocol, &mut buffer)?;
let mut chunk_packets = Vec::with_capacity((main_packet.total - 1) as usize);
for _ in 1 .. main_packet.total {
let new_data = self.socket.receive(Some(buffer_size))?;
buffer = Buffer::<LittleEndian>::new(&new_data);
let chunk_packet = SplitPacket::new(engine, protocol, &mut buffer)?;
chunk_packets.push(chunk_packet);
}
chunk_packets.sort_by(|a, b| a.number.cmp(&b.number));
for chunk_packet in chunk_packets {
main_packet.payload.extend(chunk_packet.payload);
}
let payload = main_packet.get_payload()?; // Creating a non-temporary value here
let mut new_packet_buffer = Buffer::<LittleEndian>::new(&payload); // Using the non-temporary value here
Ok(Packet::new_from_bufferer(&mut new_packet_buffer)?)
} else {
Packet::new_from_bufferer(&mut buffer)
}
}
pub fn get_kind_request_data(&mut self, engine: &Engine, protocol: u8, kind: Request) -> GDResult<Vec<u8>> {
let data = self.get_request_data(engine, protocol, kind as u8, kind.get_default_payload())?;
Ok(data)
}
/// Ask for a specific request only.
/// This function will retry fetch on timeouts.
pub fn get_request_data(&mut self, engine: &Engine, protocol: u8, kind: u8, payload: Vec<u8>) -> GDResult<Vec<u8>> {
retry_on_timeout(self.retry_count, || {
self.get_request_data_impl(engine, protocol, kind, payload.clone())
})
}
/// Ask for a specific request only (without retry logic).
fn get_request_data_impl(
&mut self,
engine: &Engine,
protocol: u8,
kind: u8,
payload: Vec<u8>,
) -> GDResult<Vec<u8>> {
let request_initial_packet = Packet::new(kind, payload).to_bytes();
self.socket.send(&request_initial_packet)?;
let mut packet = self.receive(engine, protocol, PACKET_SIZE)?;
while packet.kind == 0x41 {
// 'A'
let challenge = packet.payload;
const INFO: u8 = Request::Info as u8;
let challenge_packet = Packet::new(
kind,
match kind {
INFO => [Request::Info.get_default_payload(), challenge].concat(),
_ => challenge,
},
)
.to_bytes();
self.socket.send(&challenge_packet)?;
packet = self.receive(engine, protocol, PACKET_SIZE)?;
}
Ok(packet.payload)
}
fn get_goldsrc_server_info(buffer: &mut Buffer<LittleEndian>) -> GDResult<ServerInfo> {
let _header: u8 = buffer.read()?; //get the header (useless info)
let _address: String = buffer.read_string::<Utf8Decoder>(None)?; //get the server address (useless info)
let name = buffer.read_string::<Utf8Decoder>(None)?;
let map = buffer.read_string::<Utf8Decoder>(None)?;
let folder = buffer.read_string::<Utf8Decoder>(None)?;
let game_mode = buffer.read_string::<Utf8Decoder>(None)?;
let players = buffer.read()?;
let max_players = buffer.read()?;
let protocol = buffer.read()?;
let server_type = match buffer.read::<u8>()? {
68 => Server::Dedicated, //'D'
76 => Server::NonDedicated, //'L'
80 => Server::TV, //'P'
_ => Err(UnknownEnumCast)?,
};
let environment_type = match buffer.read::<u8>()? {
76 => Environment::Linux, //'L'
87 => Environment::Windows, //'W'
_ => Err(UnknownEnumCast)?,
};
let has_password = buffer.read::<u8>()? == 1;
let is_mod = buffer.read::<u8>()? == 1;
let mod_data = match is_mod {
false => None,
true => {
Some(ModData {
link: buffer.read_string::<Utf8Decoder>(None)?,
download_link: buffer.read_string::<Utf8Decoder>(None)?,
version: buffer.read()?,
size: buffer.read()?,
multiplayer_only: buffer.read::<u8>()? == 1,
has_own_dll: buffer.read::<u8>()? == 1,
})
}
};
let vac_secured = buffer.read::<u8>()? == 1;
let bots = buffer.read::<u8>()?;
Ok(ServerInfo {
protocol_version: protocol,
name,
map,
folder,
game_mode,
appid: 0, // not present in the obsolete response
players_online: players,
players_maximum: max_players,
players_bots: bots,
server_type,
environment_type,
has_password,
vac_secured,
the_ship: None,
game_version: "".to_string(), // a version field only for the mod
extra_data: None,
is_mod,
mod_data,
})
}
/// Get the server information's.
fn get_server_info(&mut self, engine: &Engine) -> GDResult<ServerInfo> {
let data = self.get_kind_request_data(engine, 0, Request::Info)?;
let mut buffer = Buffer::<LittleEndian>::new(&data);
if let Engine::GoldSrc(force) = engine {
if *force {
return Self::get_goldsrc_server_info(&mut buffer);
}
}
let protocol = buffer.read()?;
let name = buffer.read_string::<Utf8Decoder>(None)?;
let map = buffer.read_string::<Utf8Decoder>(None)?;
let folder = buffer.read_string::<Utf8Decoder>(None)?;
let game_mode = buffer.read_string::<Utf8Decoder>(None)?;
let mut appid = buffer.read::<u16>()? as u32;
let players = buffer.read()?;
let max_players = buffer.read()?;
let bots = buffer.read()?;
let server_type = Server::from_gldsrc(buffer.read()?)?;
let environment_type = Environment::from_gldsrc(buffer.read()?)?;
let has_password = buffer.read::<u8>()? == 1;
let vac_secured = buffer.read::<u8>()? == 1;
let the_ship = match *engine == SteamApp::THESHIP.as_engine() {
false => None,
true => {
Some(TheShip {
mode: buffer.read()?,
witnesses: buffer.read()?,
duration: buffer.read()?,
})
}
};
let game_version = buffer.read_string::<Utf8Decoder>(None)?;
let extra_data = match buffer.read::<u8>() {
Err(_) => None,
Ok(value) => {
Some(ExtraData {
port: match (value & 0x80) > 0 {
false => None,
true => Some(buffer.read()?),
},
steam_id: match (value & 0x10) > 0 {
false => None,
true => Some(buffer.read()?),
},
tv_port: match (value & 0x40) > 0 {
false => None,
true => Some(buffer.read()?),
},
tv_name: match (value & 0x40) > 0 {
false => None,
true => Some(buffer.read_string::<Utf8Decoder>(None)?),
},
keywords: match (value & 0x20) > 0 {
false => None,
true => Some(buffer.read_string::<Utf8Decoder>(None)?),
},
game_id: match (value & 0x01) > 0 {
false => None,
true => {
let gid = buffer.read()?;
appid = (gid & ((1 << 24) - 1)) as u32;
Some(gid)
}
},
})
}
};
Ok(ServerInfo {
protocol_version: protocol,
name,
map,
folder,
game_mode,
appid,
players_online: players,
players_maximum: max_players,
players_bots: bots,
server_type,
environment_type,
has_password,
vac_secured,
the_ship,
game_version,
extra_data,
is_mod: false,
mod_data: None,
})
}
/// Get the server player's.
fn get_server_players(&mut self, engine: &Engine, protocol: u8) -> GDResult<Vec<ServerPlayer>> {
let data = self.get_kind_request_data(engine, protocol, Request::Players)?;
let mut buffer = Buffer::<LittleEndian>::new(&data);
let count = buffer.read::<u8>()? as usize;
let mut players: Vec<ServerPlayer> = Vec::with_capacity(count);
for _ in 0 .. count {
buffer.move_cursor(1)?; //skip the index byte
players.push(ServerPlayer {
name: buffer.read_string::<Utf8Decoder>(None)?,
score: buffer.read()?,
duration: buffer.read()?,
deaths: match *engine == SteamApp::THESHIP.as_engine() {
false => None,
true => Some(buffer.read()?),
},
money: match *engine == SteamApp::THESHIP.as_engine() {
false => None,
true => Some(buffer.read()?),
},
});
}
Ok(players)
}
/// Get the server's rules.
fn get_server_rules(&mut self, engine: &Engine, protocol: u8) -> GDResult<HashMap<String, String>> {
let data = self.get_kind_request_data(engine, protocol, Request::Rules)?;
let mut buffer = Buffer::<LittleEndian>::new(&data);
let count = buffer.read::<u16>()? as usize;
let mut rules: HashMap<String, String> = HashMap::with_capacity(count);
for _ in 0 .. count {
let name = buffer.read_string::<Utf8Decoder>(None)?;
let value = buffer.read_string::<Utf8Decoder>(None)?;
rules.insert(name, value);
}
if *engine == SteamApp::ROR2.as_engine() {
rules.remove("Test");
}
Ok(rules)
}
}
/// 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: &SocketAddr,
engine: Engine,
gather_settings: Option<GatheringSettings>,
timeout_settings: Option<TimeoutSettings>,
) -> GDResult<Response> {
let response_gather_settings = gather_settings.unwrap_or_default();
get_response(address, engine, response_gather_settings, timeout_settings)
}
fn get_response(
address: &SocketAddr,
engine: Engine,
gather_settings: GatheringSettings,
timeout_settings: Option<TimeoutSettings>,
) -> GDResult<Response> {
let mut client = ValveProtocol::new(address, timeout_settings)?;
let info = client.get_server_info(&engine)?;
if let Engine::Source(Some(appids)) = &engine {
let mut is_specified_id = false;
if appids.0 == info.appid {
is_specified_id = true;
} else if let Some(dedicated_appid) = appids.1 {
if dedicated_appid == info.appid {
is_specified_id = true;
}
}
if !is_specified_id && gather_settings.check_app_id {
return Err(BadGame.context(format!("AppId: {}", info.appid)));
}
}
let protocol = info.protocol_version;
Ok(Response {
info,
players: match gather_settings.players {
false => None,
true => Some(client.get_server_players(&engine, protocol)?),
},
rules: match gather_settings.rules {
false => None,
true => Some(client.get_server_rules(&engine, protocol)?),
},
})
}

View file

@ -0,0 +1,556 @@
use std::collections::HashMap;
use crate::protocols::types::{CommonPlayer, CommonResponse, ExtraRequestSettings, GenericPlayer};
use crate::GDErrorKind::UnknownEnumCast;
use crate::GDResult;
use crate::{buffer::Buffer, protocols::GenericResponse};
use byteorder::LittleEndian;
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
/// The type of the server.
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum Server {
Dedicated,
NonDedicated,
TV,
}
impl Server {
pub(crate) fn from_gldsrc(value: u8) -> GDResult<Self> {
Ok(match value {
100 => Self::Dedicated, //'d'
108 => Self::NonDedicated, //'l'
112 => Self::TV, //'p'
_ => Err(UnknownEnumCast)?,
})
}
}
/// The Operating System that the server is on.
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum Environment {
Linux,
Windows,
Mac,
}
impl Environment {
pub(crate) fn from_gldsrc(value: u8) -> GDResult<Self> {
Ok(match value {
108 => Self::Linux, //'l'
119 => Self::Windows, //'w'
109 | 111 => Self::Mac, //'m' or 'o'
_ => Err(UnknownEnumCast)?,
})
}
}
/// A query response.
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq)]
pub struct Response {
pub info: ServerInfo,
pub players: Option<Vec<ServerPlayer>>,
pub rules: Option<HashMap<String, String>>,
}
impl CommonResponse for Response {
fn as_original(&self) -> GenericResponse { GenericResponse::Valve(self) }
fn name(&self) -> Option<&str> { Some(&self.info.name) }
fn game_mode(&self) -> Option<&str> { Some(&self.info.game_mode) }
fn game_version(&self) -> Option<&str> { Some(&self.info.game_version) }
fn map(&self) -> Option<&str> { Some(&self.info.map) }
fn players_maximum(&self) -> u32 { self.info.players_maximum.into() }
fn players_online(&self) -> u32 { self.info.players_online.into() }
fn players_bots(&self) -> Option<u32> { Some(self.info.players_bots.into()) }
fn has_password(&self) -> Option<bool> { Some(self.info.has_password) }
fn players(&self) -> Option<Vec<&dyn CommonPlayer>> {
self.players
.as_ref()
.map(|p| p.iter().map(|p| p as &dyn CommonPlayer).collect())
}
}
/// General server information's.
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct ServerInfo {
/// Protocol used by the server.
pub protocol_version: u8,
/// Name of the server.
pub name: String,
/// Map name.
pub map: String,
/// Name of the folder containing the game files.
pub folder: String,
/// The server-declared name of the game/game mode.
pub game_mode: String,
/// [Steam Application ID](https://developer.valvesoftware.com/wiki/Steam_Application_ID) of game.
pub appid: u32,
/// Number of players on the server.
pub players_online: u8,
/// Maximum number of players the server reports it can hold.
pub players_maximum: u8,
/// Number of bots on the server.
pub players_bots: u8,
/// Dedicated, NonDedicated or SourceTV
pub server_type: Server,
/// The Operating System that the server is on.
pub environment_type: Environment,
/// Indicates whether the server requires a password.
pub has_password: bool,
/// Indicates whether the server uses VAC.
pub vac_secured: bool,
/// [The ship](https://developer.valvesoftware.com/wiki/The_Ship) extra data
pub the_ship: Option<TheShip>,
/// Version of the game installed on the server.
pub game_version: String,
/// Some extra data that the server might provide or not.
pub extra_data: Option<ExtraData>,
/// GoldSrc only: Indicates whether the hosted game is a mod.
pub is_mod: bool,
/// GoldSrc only: If the game is a mod, provide additional data.
pub mod_data: Option<ModData>,
}
/// A server player.
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq, PartialOrd)]
pub struct ServerPlayer {
/// Player's name.
pub name: String,
/// General score.
pub score: i32,
/// How long a player has been in the server (seconds).
pub duration: f32,
/// Only for [the ship](https://developer.valvesoftware.com/wiki/The_Ship): deaths count
pub deaths: Option<u32>, // the_ship
/// Only for [the ship](https://developer.valvesoftware.com/wiki/The_Ship): money amount
pub money: Option<u32>, // the_ship
}
impl CommonPlayer for ServerPlayer {
fn as_original(&self) -> GenericPlayer { GenericPlayer::Valve(self) }
fn name(&self) -> &str { &self.name }
fn score(&self) -> Option<i32> { Some(self.score) }
}
/// Only present for [the ship](https://developer.valvesoftware.com/wiki/The_Ship).
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct TheShip {
pub mode: u8,
pub witnesses: u8,
pub duration: u8,
}
/// Some extra data that the server might provide or not.
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct ExtraData {
/// The server's game port number.
pub port: Option<u16>,
/// Server's SteamID.
pub steam_id: Option<u64>,
/// SourceTV's port.
pub tv_port: Option<u16>,
/// SourceTV's name.
pub tv_name: Option<String>,
/// Keywords that describe the server according to it.
pub keywords: Option<String>,
/// The server's 64-bit GameID.
pub game_id: Option<u64>,
}
/// Data related to GoldSrc Mod response.
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct ModData {
pub link: String,
pub download_link: String,
pub version: u32,
pub size: u32,
pub multiplayer_only: bool,
pub has_own_dll: bool,
}
pub(crate) type ExtractedData = (
Option<u16>,
Option<u64>,
Option<u16>,
Option<String>,
Option<String>,
);
pub(crate) fn get_optional_extracted_data(data: Option<ExtraData>) -> ExtractedData {
match data {
None => (None, None, None, None, None),
Some(ed) => (ed.port, ed.steam_id, ed.tv_port, ed.tv_name, ed.keywords),
}
}
#[derive(Debug, Clone)]
pub(crate) struct Packet {
pub header: u32,
pub kind: u8,
pub payload: Vec<u8>,
}
impl Packet {
pub fn new(kind: u8, payload: Vec<u8>) -> Self {
Self {
header: u32::MAX, // FF FF FF FF
kind,
payload,
}
}
pub fn new_from_bufferer(buffer: &mut Buffer<LittleEndian>) -> GDResult<Self> {
Ok(Self {
header: buffer.read::<u32>()?,
kind: buffer.read::<u8>()?,
payload: buffer.remaining_bytes().to_vec(),
})
}
pub fn to_bytes(&self) -> Vec<u8> {
let mut buf = Vec::from(self.header.to_be_bytes());
buf.push(self.kind);
buf.extend(&self.payload);
buf
}
}
/// The type of the request, see the [protocol](https://developer.valvesoftware.com/wiki/Server_queries).
#[derive(Eq, PartialEq, Copy, Clone)]
#[repr(u8)]
pub(crate) enum Request {
/// Known as `A2S_INFO`
Info = 0x54,
/// Known as `A2S_PLAYERS`
Players = 0x55,
/// Known as `A2S_RULES`
Rules = 0x56,
}
impl Request {
pub fn get_default_payload(self) -> Vec<u8> {
match self {
Self::Info => String::from("Source Engine Query\0").into_bytes(),
_ => vec![0xFF, 0xFF, 0xFF, 0xFF],
}
}
}
/// Supported steam apps
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum SteamApp {
/// Counter-Strike
COUNTERSTRIKE,
/// Creativerse
CREATIVERSE,
/// Team Fortress Classic
TFC,
/// Day of Defeat
DOD,
/// Counter-Strike: Condition Zero
CSCZ,
/// Counter-Strike: Source
CSS,
/// Day of Defeat: Source
DODS,
/// Half-Life 2 Deathmatch
HL2D,
/// Half-Life Deathmatch: Source
HLDS,
/// Team Fortress 2
TEAMFORTRESS2,
/// Left 4 Dead
LEFT4DEAD,
/// Left 4 Dead
LEFT4DEAD2,
/// Alien Swarm
ALIENSWARM,
/// Counter-Strike: Global Offensive
CSGO,
/// The Ship
THESHIP,
/// Garry's Mod
GARRYSMOD,
/// Age of Chivalry
AOC,
/// Insurgency: Modern Infantry Combat
IMIC,
/// ARMA 2: Operation Arrowhead
A2OA,
/// Project Zomboid
PROJECTZOMBOID,
/// Insurgency
INSURGENCY,
/// Sven Co-op
SCO,
/// 7 Days To Die
SD2D,
/// Rust
RUST,
/// Ballistic Overkill
BALLISTICOVERKILL,
/// Don't Starve Together
DST,
/// BrainBread 2
BRAINBREAD2,
/// Codename CURE
CODENAMECURE,
/// Black Mesa
BLACKMESA,
/// Colony Survival
COLONYSURVIVAL,
/// Avorion
AVORION,
/// Day of Infamy
DOI,
/// The Forest
THEFOREST,
/// Unturned
UNTURNED,
/// ARK: Survival Evolved
ASE,
/// Battalion 1944
BATTALION1944,
/// Insurgency: Sandstorm
INSURGENCYSANDSTORM,
/// Alien Swarm: Reactive Drop
ASRD,
/// Risk of Rain 2
ROR2,
/// Operation: Harsh Doorstop
OHD,
/// Onset
ONSET,
/// V Rising
VRISING,
/// Hell Let Loose
HLL,
/// Barotrauma
BAROTRAUMA,
}
impl SteamApp {
/// Get the specified app as engine.
pub const fn as_engine(&self) -> Engine {
match self {
Self::CSS => Engine::new_source(240),
Self::DODS => Engine::new_source(300),
Self::HL2D => Engine::new_source(320),
Self::HLDS => Engine::new_source(360),
Self::TEAMFORTRESS2 => Engine::new_source(440),
Self::LEFT4DEAD => Engine::new_source(500),
Self::LEFT4DEAD2 => Engine::new_source(550),
Self::ALIENSWARM => Engine::new_source(630),
Self::CSGO => Engine::new_source(730),
Self::THESHIP => Engine::new_source(2400),
Self::GARRYSMOD => Engine::new_source(4000),
Self::AOC => Engine::new_source(17510),
Self::IMIC => Engine::new_source(17700),
Self::A2OA => Engine::new_source(33930),
Self::PROJECTZOMBOID => Engine::new_source(108_600),
Self::INSURGENCY => Engine::new_source(222_880),
Self::SD2D => Engine::new_source(251_570),
Self::RUST => Engine::new_source(252_490),
Self::CREATIVERSE => Engine::new_source(280_790),
Self::BALLISTICOVERKILL => Engine::new_source(296_300),
Self::DST => Engine::new_source(322_320),
Self::BRAINBREAD2 => Engine::new_source(346_330),
Self::CODENAMECURE => Engine::new_source(355_180),
Self::BLACKMESA => Engine::new_source(362_890),
Self::COLONYSURVIVAL => Engine::new_source(366_090),
Self::AVORION => Engine::new_source(445_220),
Self::DOI => Engine::new_source(447_820),
Self::THEFOREST => Engine::new_source(556_450),
Self::UNTURNED => Engine::new_source(304_930),
Self::ASE => Engine::new_source(346_110),
Self::BATTALION1944 => Engine::new_source(489_940),
Self::INSURGENCYSANDSTORM => Engine::new_source(581_320),
Self::ASRD => Engine::new_source(563_560),
Self::BAROTRAUMA => Engine::new_source(602960),
Self::ROR2 => Engine::new_source(632_360),
Self::OHD => Engine::new_source_with_dedicated(736_590, 950_900),
Self::ONSET => Engine::new_source(1_105_810),
Self::VRISING => Engine::new_source(1_604_030),
Self::HLL => Engine::new_source(686_810),
_ => Engine::GoldSrc(false), // CS - 10, TFC - 20, DOD - 30, CSCZ - 80, SC - 225840
}
}
}
/// Engine type.
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum Engine {
/// A Source game, the argument represents the possible steam app ids, if
/// its **None**, let the query find it, if its **Some**, the query
/// fails if the response id is not the first one, which is the game app
/// id, or the other one, which is the dedicated server app id.
Source(Option<(u32, Option<u32>)>),
/// A GoldSrc game, the argument indicates whether to enforce
/// requesting the obsolete A2S_INFO response or not.
GoldSrc(bool),
}
impl Engine {
pub const fn new_source(appid: u32) -> Self { Self::Source(Some((appid, None))) }
pub const fn new_source_with_dedicated(appid: u32, dedicated_appid: u32) -> Self {
Self::Source(Some((appid, Some(dedicated_appid))))
}
}
/// What data to gather, purely used only with the query function.
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct GatheringSettings {
pub players: bool,
pub rules: bool,
pub check_app_id: bool,
}
impl Default for GatheringSettings {
/// Default values are true for both the players and the rules.
fn default() -> Self {
Self {
players: true,
rules: true,
check_app_id: true,
}
}
}
impl From<ExtraRequestSettings> for GatheringSettings {
fn from(value: ExtraRequestSettings) -> Self {
let default = Self::default();
Self {
players: value.gather_players.unwrap_or(default.players),
rules: value.gather_rules.unwrap_or(default.rules),
check_app_id: value.check_app_id.unwrap_or(default.check_app_id),
}
}
}
/// 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 {
use super::{Server, ServerPlayer};
use crate::protocols::valve::types::get_optional_extracted_data;
use std::collections::HashMap;
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
/// A player's details.
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq, PartialOrd)]
pub struct Player {
/// Player's name.
pub name: String,
/// Player's score.
pub score: i32,
/// How long a player has been in the server (seconds).
pub duration: f32,
}
impl Player {
pub fn from_valve_response(player: &ServerPlayer) -> Self {
Self {
name: player.name.clone(),
score: player.score,
duration: player.duration,
}
}
}
/// The query response.
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq)]
pub struct Response {
/// Protocol used by the server.
pub protocol: u8,
/// Name of the server.
pub name: String,
/// Map name.
pub map: String,
/// The name of the game.
pub game: String,
/// Server's app id.
pub appid: u32,
/// Number of players on the server.
pub players_online: u8,
/// Details about the server's players (not all players necessarily).
pub players_details: Vec<Player>,
/// Maximum number of players the server reports it can hold.
pub players_maximum: u8,
/// Number of bots on the server.
pub players_bots: u8,
/// Dedicated, NonDedicated or SourceTV
pub server_type: Server,
/// Indicates whether the server requires a password.
pub has_password: bool,
/// Indicated whether the server uses VAC.
pub vac_secured: bool,
/// Version of the game installed on the server.
pub version: String,
/// The server's reported connection port.
pub port: Option<u16>,
/// Server's SteamID.
pub steam_id: Option<u64>,
/// SourceTV's connection port.
pub tv_port: Option<u16>,
/// SourceTV's name.
pub tv_name: Option<String>,
/// Keywords that describe the server according to it.
pub keywords: Option<String>,
/// Server's rules.
pub rules: HashMap<String, String>,
}
impl Response {
pub fn new_from_valve_response(response: super::Response) -> Self {
let (port, steam_id, tv_port, tv_name, keywords) = get_optional_extracted_data(response.info.extra_data);
Self {
protocol: response.info.protocol_version,
name: response.info.name,
map: response.info.map,
game: response.info.game_mode,
appid: response.info.appid,
players_online: response.info.players_online,
players_details: response
.players
.unwrap_or_default()
.iter()
.map(Player::from_valve_response)
.collect(),
players_maximum: response.info.players_maximum,
players_bots: response.info.players_bots,
server_type: response.info.server_type,
has_password: response.info.has_password,
vac_secured: response.info.vac_secured,
version: response.info.game_version,
port,
steam_id,
tv_port,
tv_name,
keywords,
rules: response.rules.unwrap_or_default(),
}
}
}
}

View file

@ -0,0 +1,4 @@
//! Services that are currently implemented.
/// Reference: [Master Server Query Protocol](https://developer.valvesoftware.com/wiki/Master_Server_Query_Protocol)
pub mod valve_master_server;

View file

@ -0,0 +1,7 @@
/// The implementation.
pub mod service;
/// All types used by the implementation.
pub mod types;
pub use service::*;
pub use types::*;

View file

@ -0,0 +1,155 @@
use crate::{
buffer::Buffer,
socket::{Socket, UdpSocket},
valve_master_server::{Region, SearchFilters},
GDErrorKind::PacketBad,
GDResult,
};
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
use byteorder::BigEndian;
/// The default master ip, which is the one for Source.
pub fn default_master_address() -> SocketAddr {
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(208, 64, 201, 194)), 27011) // hl2master.steampowered.com
}
fn construct_payload(region: Region, filters: &Option<SearchFilters>, last_ip: &str, last_port: u16) -> Vec<u8> {
let filters_bytes: Vec<u8> = filters
.as_ref()
.map_or_else(|| vec![0x00], |f| f.to_bytes());
let region_byte = &[region as u8];
[
// Packet has to begin with the character '1'
&[0x31],
// The region byte is next
region_byte,
// The last fetched ip as a string
last_ip.as_bytes(),
// Followed by an ':'
&[b':'],
// And the port, as a string
last_port.to_string().as_bytes(),
// Which needs to end with a NULL byte
&[0x00],
// Then the filters
&filters_bytes,
]
.concat()
}
/// The implementation, use this if you want to keep the same socket.
pub struct ValveMasterServer {
socket: UdpSocket,
}
impl ValveMasterServer {
/// Construct a new struct.
pub fn new(master_address: &SocketAddr) -> GDResult<Self> {
let socket = UdpSocket::new(master_address)?;
socket.apply_timeout(&None)?;
Ok(Self { socket })
}
/// Make just a single query, providing `0.0.0.0` as the last ip and `0` as
/// the last port will give the initial packet.
pub fn query_specific(
&mut self,
region: Region,
search_filters: &Option<SearchFilters>,
last_address_ip: &str,
last_address_port: u16,
) -> GDResult<Vec<(IpAddr, u16)>> {
let payload = construct_payload(region, search_filters, last_address_ip, last_address_port);
self.socket.send(&payload)?;
let received_data = self.socket.receive(Some(1400))?;
let mut buf = Buffer::<BigEndian>::new(&received_data);
if buf.read::<u32>()? != u32::MAX || buf.read::<u16>()? != 26122 {
return Err(PacketBad.context("Expected 4294967295 followed by 26122"));
}
let mut ips: Vec<(IpAddr, u16)> = Vec::new();
while buf.remaining_length() > 0 {
let ip = IpAddr::V4(Ipv4Addr::new(
buf.read::<u8>()?,
buf.read::<u8>()?,
buf.read::<u8>()?,
buf.read::<u8>()?,
));
let port = buf.read::<u16>()?;
ips.push((ip, port));
}
Ok(ips)
}
/// Make a complete query.
pub fn query(&mut self, region: Region, search_filters: Option<SearchFilters>) -> GDResult<Vec<(IpAddr, u16)>> {
let mut ips: Vec<(IpAddr, u16)> = Vec::new();
let mut exit_fetching = false;
let mut last_ip: String = "0.0.0.0".to_string();
let mut last_port: u16 = 0;
while !exit_fetching {
let new_ips = self.query_specific(region, &search_filters, last_ip.as_str(), last_port)?;
match new_ips.last() {
None => exit_fetching = true,
Some((latest_ip, latest_port)) => {
let mut remove_last = false;
let latest_ip_string = latest_ip.to_string();
if latest_ip_string == "0.0.0.0" && *latest_port == 0 {
exit_fetching = true;
remove_last = true;
} else if latest_ip_string == last_ip && *latest_port == last_port {
exit_fetching = true;
} else {
last_ip = latest_ip_string;
last_port = *latest_port;
}
ips.extend(new_ips);
if remove_last {
ips.pop();
}
}
}
}
Ok(ips)
}
}
/// Take only the first response of (what would be a) complete query. This is
/// faster as it results in less packets being sent, received and processed but
/// yields less ips.
pub fn query_singular(region: Region, search_filters: Option<SearchFilters>) -> GDResult<Vec<(IpAddr, u16)>> {
let mut master_server = ValveMasterServer::new(&default_master_address())?;
let mut ips = master_server.query_specific(region, &search_filters, "0.0.0.0", 0)?;
if let Some((last_ip, last_port)) = ips.last() {
if last_ip.to_string() == "0.0.0.0" && *last_port == 0 {
ips.pop();
}
}
Ok(ips)
}
/// Make a complete query.
pub fn query(region: Region, search_filters: Option<SearchFilters>) -> GDResult<Vec<(IpAddr, u16)>> {
let mut master_server = ValveMasterServer::new(&default_master_address())?;
master_server.query(region, search_filters)
}

View file

@ -0,0 +1,234 @@
use std::collections::HashMap;
use std::mem::Discriminant;
/// A query filter.
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum Filter {
IsSecured(bool),
RunsMap(String),
CanHavePassword(bool),
CanBeEmpty(bool),
IsEmpty(bool),
CanBeFull(bool),
RunsAppID(u32),
NotAppID(u32),
HasTags(Vec<String>),
MatchName(String),
MatchVersion(String),
/// Restrict to only a server if an IP hosts (on different ports) multiple
/// servers.
RestrictUniqueIP(bool),
/// Query for servers on a specific address.
OnAddress(String),
Whitelisted(bool),
SpectatorProxy(bool),
IsDedicated(bool),
RunsLinux(bool),
HasGameDir(String),
}
const fn bool_as_char_u8(b: &bool) -> u8 {
match b {
true => b'1',
false => b'0',
}
}
impl Filter {
pub(crate) fn to_bytes(&self) -> Vec<u8> {
let mut bytes: Vec<u8> = Vec::new();
match self {
Self::IsSecured(secured) => {
bytes = b"\\secure\\".to_vec();
bytes.extend([bool_as_char_u8(secured)]);
}
Self::RunsMap(map) => {
bytes = b"\\map\\".to_vec();
bytes.extend(map.as_bytes());
}
Self::CanHavePassword(password) => {
bytes = b"\\password\\".to_vec();
bytes.extend([bool_as_char_u8(password)]);
}
Self::CanBeEmpty(empty) => {
bytes = b"\\empty\\".to_vec();
bytes.extend([bool_as_char_u8(empty)]);
}
Self::CanBeFull(full) => {
bytes = b"\\full\\".to_vec();
bytes.extend([bool_as_char_u8(full)]);
}
Self::RunsAppID(id) => {
bytes = b"\\appid\\".to_vec();
bytes.extend(id.to_string().as_bytes());
}
Self::HasTags(tags) => {
if !tags.is_empty() {
bytes = b"\\gametype\\".to_vec();
for tag in tags.iter() {
bytes.extend(tag.as_bytes());
bytes.extend([b',']);
}
bytes.pop();
}
}
Self::NotAppID(id) => {
bytes = b"\\napp\\".to_vec();
bytes.extend(id.to_string().as_bytes());
}
Self::IsEmpty(empty) => {
bytes = b"\\noplayers\\".to_vec();
bytes.extend([bool_as_char_u8(empty)]);
}
Self::MatchName(name) => {
bytes = b"\\name_match\\".to_vec();
bytes.extend(name.as_bytes());
}
Self::MatchVersion(version) => {
bytes = b"\\version_match\\".to_vec();
bytes.extend(version.as_bytes());
}
Self::RestrictUniqueIP(unique) => {
bytes = b"\\collapse_addr_hash\\".to_vec();
bytes.extend([bool_as_char_u8(unique)]);
}
Self::OnAddress(address) => {
bytes = b"\\gameaddr\\".to_vec();
bytes.extend(address.as_bytes());
}
Self::Whitelisted(whitelisted) => {
bytes = b"\\white\\".to_vec();
bytes.extend([bool_as_char_u8(whitelisted)]);
}
Self::SpectatorProxy(condition) => {
bytes = b"\\proxy\\".to_vec();
bytes.extend([bool_as_char_u8(condition)]);
}
Self::IsDedicated(dedicated) => {
bytes = b"\\dedicated\\".to_vec();
bytes.extend([bool_as_char_u8(dedicated)]);
}
Self::RunsLinux(linux) => {
bytes = b"\\linux\\".to_vec();
bytes.extend([bool_as_char_u8(linux)]);
}
Self::HasGameDir(game_dir) => {
bytes = b"\\gamedir\\".to_vec();
bytes.extend(game_dir.as_bytes());
}
}
bytes
}
}
/// Query search filters.
/// An example of constructing one:
/// ```rust
/// use gamedig::valve_master_server::{Filter, SearchFilters};
///
/// let search_filters = SearchFilters::new()
/// .insert(Filter::RunsAppID(440))
/// .insert(Filter::IsEmpty(false))
/// .insert(Filter::CanHavePassword(false));
/// ```
/// This will construct filters that search for servers that can't have a
/// password, are not empty and run App ID 440.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SearchFilters {
filters: HashMap<Discriminant<Filter>, Filter>,
nor_filters: HashMap<Discriminant<Filter>, Filter>,
nand_filters: HashMap<Discriminant<Filter>, Filter>,
}
impl Default for SearchFilters {
fn default() -> Self { Self::new() }
}
impl SearchFilters {
pub fn new() -> Self {
Self {
filters: HashMap::new(),
nor_filters: HashMap::new(),
nand_filters: HashMap::new(),
}
}
pub fn insert(self, filter: Filter) -> Self {
let mut updated_fitler = self.filters;
updated_fitler.insert(std::mem::discriminant(&filter), filter);
Self {
filters: updated_fitler,
nand_filters: self.nand_filters,
nor_filters: self.nor_filters,
}
}
pub fn insert_nand(self, filter: Filter) -> Self {
let mut updated_fitler = self.nor_filters;
updated_fitler.insert(std::mem::discriminant(&filter), filter);
Self {
filters: self.filters,
nand_filters: self.nand_filters,
nor_filters: updated_fitler,
}
}
pub fn insert_nor(self, filter: Filter) -> Self {
let mut updated_fitler = self.nand_filters;
updated_fitler.insert(std::mem::discriminant(&filter), filter);
Self {
filters: self.filters,
nand_filters: updated_fitler,
nor_filters: self.nor_filters,
}
}
fn special_filter_to_bytes(name: &str, filters: &HashMap<Discriminant<Filter>, Filter>) -> Vec<u8> {
let mut bytes = Vec::new();
if !filters.is_empty() {
bytes.extend(name.as_bytes());
bytes.extend(filters.len().to_string().as_bytes());
for filter in filters.values() {
bytes.extend(filter.to_bytes());
}
}
bytes
}
pub(crate) fn to_bytes(&self) -> Vec<u8> {
let mut bytes: Vec<u8> = Vec::new();
for filter in self.filters.values() {
bytes.extend(filter.to_bytes())
}
bytes.extend(Self::special_filter_to_bytes("nand", &self.nand_filters));
bytes.extend(Self::special_filter_to_bytes("nor", &self.nor_filters));
bytes.extend([0x00]);
bytes
}
}
/// The region that you want to query server for.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
#[repr(u8)]
pub enum Region {
UsEast = 0x00,
UsWest = 0x01,
AmericaSouth = 0x02,
Europe = 0x03,
Asia = 0x04,
Australia = 0x05,
MiddleEast = 0x06,
Africa = 0x07,
Others = 0xFF,
}

177
crates/lib/src/socket.rs Normal file
View file

@ -0,0 +1,177 @@
use crate::{
protocols::types::TimeoutSettings,
GDErrorKind::{PacketReceive, PacketSend, SocketBind, SocketConnect},
GDResult,
};
use std::net::SocketAddr;
use std::{
io::{Read, Write},
net,
};
const DEFAULT_PACKET_SIZE: usize = 1024;
pub trait Socket {
fn new(address: &SocketAddr) -> GDResult<Self>
where Self: Sized;
fn apply_timeout(&self, timeout_settings: &Option<TimeoutSettings>) -> GDResult<()>;
fn send(&mut self, data: &[u8]) -> GDResult<()>;
fn receive(&mut self, size: Option<usize>) -> GDResult<Vec<u8>>;
fn port(&self) -> u16;
}
pub struct TcpSocket {
socket: net::TcpStream,
address: SocketAddr,
}
impl Socket for TcpSocket {
fn new(address: &SocketAddr) -> GDResult<Self> {
Ok(Self {
socket: net::TcpStream::connect(address).map_err(|e| SocketConnect.context(e))?,
address: *address,
})
}
fn apply_timeout(&self, timeout_settings: &Option<TimeoutSettings>) -> GDResult<()> {
let (read, write) = TimeoutSettings::get_read_and_write_or_defaults(timeout_settings);
self.socket.set_read_timeout(read).unwrap(); // unwrapping because TimeoutSettings::new
self.socket.set_write_timeout(write).unwrap(); // checks if these are 0 and throws an error
Ok(())
}
fn send(&mut self, data: &[u8]) -> GDResult<()> {
self.socket.write(data).map_err(|e| PacketSend.context(e))?;
Ok(())
}
fn receive(&mut self, size: Option<usize>) -> GDResult<Vec<u8>> {
let mut buf = Vec::with_capacity(size.unwrap_or(DEFAULT_PACKET_SIZE));
self.socket
.read_to_end(&mut buf)
.map_err(|e| PacketReceive.context(e))?;
Ok(buf)
}
fn port(&self) -> u16 { self.address.port() }
}
pub struct UdpSocket {
socket: net::UdpSocket,
address: SocketAddr,
}
impl Socket for UdpSocket {
fn new(address: &SocketAddr) -> GDResult<Self> {
let socket = net::UdpSocket::bind("0.0.0.0:0").map_err(|e| SocketBind.context(e))?;
Ok(Self {
socket,
address: *address,
})
}
fn apply_timeout(&self, timeout_settings: &Option<TimeoutSettings>) -> GDResult<()> {
let (read, write) = TimeoutSettings::get_read_and_write_or_defaults(timeout_settings);
self.socket.set_read_timeout(read).unwrap(); // unwrapping because TimeoutSettings::new
self.socket.set_write_timeout(write).unwrap(); // checks if these are 0 and throws an error
Ok(())
}
fn send(&mut self, data: &[u8]) -> GDResult<()> {
self.socket
.send_to(data, self.address)
.map_err(|e| PacketSend.context(e))?;
Ok(())
}
fn receive(&mut self, size: Option<usize>) -> GDResult<Vec<u8>> {
let mut buf: Vec<u8> = vec![0; size.unwrap_or(DEFAULT_PACKET_SIZE)];
let (number_of_bytes_received, _) = self
.socket
.recv_from(&mut buf)
.map_err(|e| PacketReceive.context(e))?;
Ok(buf[.. number_of_bytes_received].to_vec())
}
fn port(&self) -> u16 { self.address.port() }
}
#[cfg(test)]
mod tests {
use std::thread;
use super::*;
#[test]
fn test_tcp_socket_send_and_receive() {
// Spawn a thread to run the server
let listener = net::TcpListener::bind("127.0.0.1:0").unwrap();
let bound_address = listener.local_addr().unwrap();
let server_thread = thread::spawn(move || {
let (mut stream, _) = listener.accept().unwrap();
let mut buf = [0; 1024];
let _ = stream.read(&mut buf).unwrap();
let _ = stream.write(&buf).unwrap();
});
// Create a TCP socket and send a message to the server
let mut socket = TcpSocket::new(&bound_address).unwrap();
let message = b"hello, world!";
socket.send(message).unwrap();
// Receive the response from the server
let received_message: Vec<u8> = socket
.receive(None)
.unwrap()
// Iterate over the buffer and remove 0s that are alone in the buffer
// just added to pass default size
.into_iter()
.filter(|&x| x != 0)
.collect();
server_thread.join().expect("server thread panicked");
assert_eq!(message, &received_message[..]);
}
#[test]
fn test_udp_socket_send_and_receive() {
// Spawn a thread to run the server
let socket = net::UdpSocket::bind("127.0.0.1:0").unwrap();
let bound_address = socket.local_addr().unwrap();
let server_thread = thread::spawn(move || {
let mut buf = [0; 1024];
let (_, src_addr) = socket.recv_from(&mut buf).unwrap();
socket.send_to(&buf, src_addr).unwrap();
});
// Create a UDP socket and send a message to the server
let mut socket = UdpSocket::new(&bound_address).unwrap();
let message = b"hello, world!";
socket.send(message).unwrap();
// Receive the response from the server
let received_message: Vec<u8> = socket
.receive(None)
.unwrap()
// Iterate over the buffer and remove 0s that are alone in the buffer
// just added to pass default size
.into_iter()
.filter(|&x| x != 0)
.collect();
server_thread.join().expect("server thread panicked");
assert_eq!(message, &received_message[..]);
}
}

108
crates/lib/src/utils.rs Normal file
View file

@ -0,0 +1,108 @@
use crate::GDErrorKind::{PacketOverflow, PacketReceive, PacketSend, PacketUnderflow};
use crate::GDResult;
use std::cmp::Ordering;
pub fn error_by_expected_size(expected: usize, size: usize) -> GDResult<()> {
match size.cmp(&expected) {
Ordering::Greater => Err(PacketOverflow.into()),
Ordering::Less => Err(PacketUnderflow.into()),
Ordering::Equal => Ok(()),
}
}
pub const fn u8_lower_upper(n: u8) -> (u8, u8) { (n & 15, n >> 4) }
/// Run a closure `retry_count+1` times while it returns [PacketReceive] or
/// [PacketSend] errors, returning the first success, other Error, or after
/// `retry_count+1` tries the last [PacketReceive] or [PacketSend] error.
pub fn retry_on_timeout<T>(mut retry_count: usize, mut fetch: impl FnMut() -> GDResult<T>) -> GDResult<T> {
let mut last_err = PacketReceive.context("Retry count was 0");
retry_count += 1;
while retry_count > 0 {
last_err = match fetch() {
Ok(r) => return Ok(r),
Err(e) if e.kind == PacketReceive || e.kind == PacketSend => e,
Err(e) => return Err(e),
};
retry_count -= 1;
}
Err(last_err)
}
#[cfg(test)]
mod tests {
use super::retry_on_timeout;
use crate::{
GDErrorKind::{PacketBad, PacketReceive, PacketSend},
GDResult,
};
#[test]
fn u8_lower_upper() {
assert_eq!(super::u8_lower_upper(171), (11, 10));
}
#[test]
fn error_by_expected_size() {
assert!(super::error_by_expected_size(69, 69).is_ok());
assert!(super::error_by_expected_size(69, 68).is_err());
assert!(super::error_by_expected_size(69, 70).is_err());
}
#[test]
fn retry_success_on_first() {
let r = retry_on_timeout(0, || Ok(()));
assert!(r.is_ok());
}
#[test]
fn retry_no_success() {
let r: GDResult<()> = retry_on_timeout(100, || Err(PacketSend.context("test")));
assert!(r.is_err());
assert_eq!(r.unwrap_err().kind, PacketSend);
}
#[test]
fn retry_success_on_third() {
let mut i = 0u8;
let r = retry_on_timeout(2, || {
i += 1;
if i < 3 {
Err(PacketReceive.context("test"))
} else {
Ok(())
}
});
assert!(r.is_ok());
}
#[test]
fn retry_success_on_third_but_less_retries() {
let mut i = 0u8;
let r = retry_on_timeout(1, || {
i += 1;
if i < 3 {
Err(PacketReceive.context("test"))
} else {
Ok(())
}
});
assert!(r.is_err());
assert_eq!(r.unwrap_err().kind, PacketReceive);
}
#[test]
fn retry_with_non_timeout_error() {
let mut i = 0u8;
let r = retry_on_timeout(50, || {
i += 1;
match i {
1 => Err(PacketSend.context("test")),
2 => Err(PacketBad.context("test")),
_ => Ok(()),
}
});
assert!(r.is_err());
assert_eq!(r.unwrap_err().kind, PacketBad);
}
}