You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
554 lines
18 KiB
554 lines
18 KiB
// SPDX-License-Identifier: GPL-3.0-only |
|
// SPDX-FileCopyrightText: 2023 Denis Drakhnia <numas13@gmail.com> |
|
|
|
//! Server query filter |
|
//! |
|
//! # Supported filters: |
|
//! |
|
//! | Filter | Type | Description | Examples | |
|
//! | --------- | ---- | ------------------------------ | -------- | |
|
//! | map | str | Map name | `crossfire`, `de_dust` | |
|
//! | gamedir | str | Game directory | `valve`, `cstrike` | |
|
//! | dedicated | bool | Server running dedicated | `0`, `1` | |
|
//! | lan | bool | Server is LAN | `0`, `1` | |
|
//! | nat | bool | Server behind NAT | `0`, `1` | |
|
//! | noplayers | bool | Server is empty | `0`, `1` | |
|
//! | empty | bool | Server is not empty | `0`, `1` | |
|
//! | full | bool | Server is not full | `0`, `1` | |
|
//! | password | bool | Server is password prodected | `0`, `1` | |
|
//! | secure | bool | Server using anti-cheat | `0`, `1` | |
|
//! | bots | bool | Server has bots | `0`, `1` | |
|
//! |
|
//! # Examples: |
|
//! |
|
//! Filter `\gamedir\valve\full\1\bots\0\password\0` will select server if: |
|
//! |
|
//! * It is Half-Life server |
|
//! * Is not full |
|
//! * Do not have bots |
|
//! * Is not protected by a password |
|
|
|
use std::fmt; |
|
use std::net::SocketAddrV4; |
|
use std::num::ParseIntError; |
|
use std::str::FromStr; |
|
|
|
use bitflags::bitflags; |
|
use log::debug; |
|
|
|
use crate::cursor::{Cursor, GetKeyValue, PutKeyValue}; |
|
use crate::server::{ServerAdd, ServerFlags, ServerType}; |
|
use crate::types::Str; |
|
use crate::{Error, ServerInfo}; |
|
|
|
bitflags! { |
|
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] |
|
pub struct FilterFlags: u16 { |
|
/// Servers running dedicated |
|
const DEDICATED = 1 << 0; |
|
/// Servers using anti-cheat technology (VAC, but potentially others as well) |
|
const SECURE = 1 << 1; |
|
/// Servers that are not password protected |
|
const PASSWORD = 1 << 2; |
|
/// Servers that are not empty |
|
const NOT_EMPTY = 1 << 3; |
|
/// Servers that are not full |
|
const FULL = 1 << 4; |
|
/// Servers that are empty |
|
const NOPLAYERS = 1 << 5; |
|
/// Servers that are behind NAT |
|
const NAT = 1 << 6; |
|
/// Servers that are LAN |
|
const LAN = 1 << 7; |
|
/// Servers that has bots |
|
const BOTS = 1 << 8; |
|
} |
|
} |
|
|
|
impl<T> From<&ServerAdd<T>> for FilterFlags { |
|
fn from(info: &ServerAdd<T>) -> Self { |
|
let mut flags = Self::empty(); |
|
|
|
flags.set(Self::DEDICATED, info.server_type == ServerType::Dedicated); |
|
flags.set(Self::SECURE, info.flags.contains(ServerFlags::SECURE)); |
|
flags.set(Self::PASSWORD, info.flags.contains(ServerFlags::PASSWORD)); |
|
flags.set(Self::NOT_EMPTY, info.players > 0); |
|
flags.set(Self::FULL, info.players >= info.max); |
|
flags.set(Self::NOPLAYERS, info.players == 0); |
|
flags.set(Self::NAT, info.flags.contains(ServerFlags::NAT)); |
|
flags.set(Self::LAN, info.flags.contains(ServerFlags::LAN)); |
|
flags.set(Self::BOTS, info.flags.contains(ServerFlags::BOTS)); |
|
|
|
flags |
|
} |
|
} |
|
|
|
#[derive(Copy, Clone, Default, PartialEq, Eq, PartialOrd, Ord)] |
|
pub struct Version { |
|
pub major: u8, |
|
pub minor: u8, |
|
pub patch: u8, |
|
} |
|
|
|
impl Version { |
|
pub const fn new(major: u8, minor: u8) -> Self { |
|
Self::with_patch(major, minor, 0) |
|
} |
|
|
|
pub const fn with_patch(major: u8, minor: u8, patch: u8) -> Self { |
|
Self { |
|
major, |
|
minor, |
|
patch, |
|
} |
|
} |
|
} |
|
|
|
impl fmt::Debug for Version { |
|
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { |
|
write!(fmt, "{}.{}", self.major, self.minor)?; |
|
if self.patch != 0 { |
|
write!(fmt, ".{}", self.patch)?; |
|
} |
|
Ok(()) |
|
} |
|
} |
|
|
|
impl fmt::Display for Version { |
|
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { |
|
<Self as fmt::Debug>::fmt(self, fmt) |
|
} |
|
} |
|
|
|
impl FromStr for Version { |
|
type Err = ParseIntError; |
|
|
|
fn from_str(s: &str) -> Result<Self, Self::Err> { |
|
let (major, tail) = s.split_once('.').unwrap_or((s, "0")); |
|
let (minor, patch) = tail.split_once('.').unwrap_or((tail, "0")); |
|
let major = major.parse()?; |
|
let minor = minor.parse()?; |
|
let patch = patch.parse()?; |
|
Ok(Self::with_patch(major, minor, patch)) |
|
} |
|
} |
|
|
|
impl GetKeyValue<'_> for Version { |
|
fn get_key_value(cur: &mut Cursor) -> Result<Self, Error> { |
|
Self::from_str(cur.get_key_value()?).map_err(|_| Error::InvalidPacket) |
|
} |
|
} |
|
|
|
impl PutKeyValue for Version { |
|
fn put_key_value<'a, 'b>( |
|
&self, |
|
cur: &'b mut crate::cursor::CursorMut<'a>, |
|
) -> Result<&'b mut crate::cursor::CursorMut<'a>, Error> { |
|
cur.put_key_value(self.major)? |
|
.put_u8(b'.')? |
|
.put_key_value(self.minor) |
|
} |
|
} |
|
|
|
#[derive(Clone, Debug, Default, PartialEq, Eq)] |
|
pub struct Filter<'a> { |
|
/// Servers running the specified modification (ex. cstrike) |
|
pub gamedir: Option<Str<&'a [u8]>>, |
|
/// Servers running the specified map (ex. cs_italy) |
|
pub map: Option<Str<&'a [u8]>>, |
|
/// Client version. |
|
pub clver: Option<Version>, |
|
pub key: Option<u32>, |
|
|
|
pub flags: FilterFlags, |
|
pub flags_mask: FilterFlags, |
|
} |
|
|
|
impl Filter<'_> { |
|
pub fn insert_flag(&mut self, flag: FilterFlags, value: bool) { |
|
self.flags.set(flag, value); |
|
self.flags_mask.insert(flag); |
|
} |
|
|
|
pub fn matches(&self, _addr: SocketAddrV4, info: &ServerInfo) -> bool { |
|
!((info.flags & self.flags_mask) != self.flags |
|
|| self.gamedir.map_or(false, |s| *s != &*info.gamedir) |
|
|| self.map.map_or(false, |s| *s != &*info.map)) |
|
} |
|
} |
|
|
|
impl<'a> TryFrom<&'a [u8]> for Filter<'a> { |
|
type Error = Error; |
|
|
|
fn try_from(src: &'a [u8]) -> Result<Self, Self::Error> { |
|
let mut cur = Cursor::new(src); |
|
let mut filter = Self::default(); |
|
|
|
loop { |
|
let key = match cur.get_key_raw().map(Str) { |
|
Ok(s) => s, |
|
Err(Error::UnexpectedEnd) => break, |
|
Err(e) => return Err(e), |
|
}; |
|
|
|
match *key { |
|
b"dedicated" => filter.insert_flag(FilterFlags::DEDICATED, cur.get_key_value()?), |
|
b"secure" => filter.insert_flag(FilterFlags::SECURE, cur.get_key_value()?), |
|
b"gamedir" => filter.gamedir = Some(cur.get_key_value()?), |
|
b"map" => filter.map = Some(cur.get_key_value()?), |
|
b"empty" => filter.insert_flag(FilterFlags::NOT_EMPTY, cur.get_key_value()?), |
|
b"full" => filter.insert_flag(FilterFlags::FULL, cur.get_key_value()?), |
|
b"password" => filter.insert_flag(FilterFlags::PASSWORD, cur.get_key_value()?), |
|
b"noplayers" => filter.insert_flag(FilterFlags::NOPLAYERS, cur.get_key_value()?), |
|
b"clver" => { |
|
filter.clver = Some( |
|
cur.get_key_value::<&str>()? |
|
.parse() |
|
.map_err(|_| Error::InvalidPacket)?, |
|
); |
|
} |
|
b"nat" => filter.insert_flag(FilterFlags::NAT, cur.get_key_value()?), |
|
b"lan" => filter.insert_flag(FilterFlags::LAN, cur.get_key_value()?), |
|
b"bots" => filter.insert_flag(FilterFlags::BOTS, cur.get_key_value()?), |
|
b"key" => { |
|
filter.key = { |
|
let s = cur.get_key_value::<&str>()?; |
|
let x = u32::from_str_radix(s, 16).map_err(|_| Error::InvalidPacket)?; |
|
Some(x) |
|
} |
|
} |
|
_ => { |
|
// skip unknown fields |
|
let value = Str(cur.get_key_value_raw()?); |
|
debug!("Invalid Filter field \"{}\" = \"{}\"", key, value); |
|
} |
|
} |
|
} |
|
|
|
Ok(filter) |
|
} |
|
} |
|
|
|
impl fmt::Display for &Filter<'_> { |
|
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { |
|
macro_rules! display_flag { |
|
($n:expr, $f:expr) => { |
|
if self.flags_mask.contains($f) { |
|
let flag = if self.flags.contains($f) { '1' } else { '0' }; |
|
write!(fmt, "\\{}\\{}", $n, flag)?; |
|
} |
|
}; |
|
} |
|
|
|
display_flag!("dedicated", FilterFlags::DEDICATED); |
|
display_flag!("secure", FilterFlags::SECURE); |
|
if let Some(s) = self.gamedir { |
|
write!(fmt, "\\gamedir\\{}", s)?; |
|
} |
|
display_flag!("secure", FilterFlags::SECURE); |
|
if let Some(s) = self.map { |
|
write!(fmt, "\\map\\{}", s)?; |
|
} |
|
display_flag!("empty", FilterFlags::NOT_EMPTY); |
|
display_flag!("full", FilterFlags::FULL); |
|
display_flag!("password", FilterFlags::PASSWORD); |
|
display_flag!("noplayers", FilterFlags::NOPLAYERS); |
|
if let Some(v) = self.clver { |
|
write!(fmt, "\\clver\\{}", v)?; |
|
} |
|
display_flag!("nat", FilterFlags::NAT); |
|
display_flag!("lan", FilterFlags::LAN); |
|
display_flag!("bots", FilterFlags::BOTS); |
|
if let Some(x) = self.key { |
|
write!(fmt, "\\key\\{:x}", x)?; |
|
} |
|
|
|
Ok(()) |
|
} |
|
} |
|
|
|
#[cfg(test)] |
|
mod tests { |
|
use super::super::cursor::CursorMut; |
|
use super::super::types::Str; |
|
use super::*; |
|
|
|
macro_rules! tests { |
|
($($name:ident$(($($predefined_f:ident: $predefined_v:expr),+ $(,)?))? { |
|
$($src:expr => { |
|
$($field:ident: $value:expr),* $(,)? |
|
})+ |
|
})+) => { |
|
$(#[test] |
|
fn $name() { |
|
let predefined = Filter { |
|
$($($predefined_f: $predefined_v,)+)? |
|
.. Filter::default() |
|
}; |
|
$(assert_eq!( |
|
Filter::try_from($src as &[u8]), |
|
Ok(Filter { |
|
$($field: $value,)* |
|
..predefined |
|
}) |
|
);)+ |
|
})+ |
|
}; |
|
} |
|
|
|
tests! { |
|
parse_gamedir { |
|
b"\\gamedir\\valve" => { |
|
gamedir: Some(Str(&b"valve"[..])), |
|
} |
|
} |
|
parse_map { |
|
b"\\map\\crossfire" => { |
|
map: Some(Str(&b"crossfire"[..])), |
|
} |
|
} |
|
parse_clver { |
|
b"\\clver\\0.20" => { |
|
clver: Some(Version::new(0, 20)), |
|
} |
|
} |
|
parse_dedicated(flags_mask: FilterFlags::DEDICATED) { |
|
b"\\dedicated\\0" => {} |
|
b"\\dedicated\\1" => { |
|
flags: FilterFlags::DEDICATED, |
|
} |
|
} |
|
parse_secure(flags_mask: FilterFlags::SECURE) { |
|
b"\\secure\\0" => {} |
|
b"\\secure\\1" => { |
|
flags: FilterFlags::SECURE, |
|
} |
|
} |
|
parse_password(flags_mask: FilterFlags::PASSWORD) { |
|
b"\\password\\0" => {} |
|
b"\\password\\1" => { |
|
flags: FilterFlags::PASSWORD, |
|
} |
|
} |
|
parse_empty(flags_mask: FilterFlags::NOT_EMPTY) { |
|
b"\\empty\\0" => {} |
|
b"\\empty\\1" => { |
|
flags: FilterFlags::NOT_EMPTY, |
|
} |
|
} |
|
parse_full(flags_mask: FilterFlags::FULL) { |
|
b"\\full\\0" => {} |
|
b"\\full\\1" => { |
|
flags: FilterFlags::FULL, |
|
} |
|
} |
|
parse_noplayers(flags_mask: FilterFlags::NOPLAYERS) { |
|
b"\\noplayers\\0" => {} |
|
b"\\noplayers\\1" => { |
|
flags: FilterFlags::NOPLAYERS, |
|
} |
|
} |
|
parse_nat(flags_mask: FilterFlags::NAT) { |
|
b"\\nat\\0" => {} |
|
b"\\nat\\1" => { |
|
flags: FilterFlags::NAT, |
|
} |
|
} |
|
parse_lan(flags_mask: FilterFlags::LAN) { |
|
b"\\lan\\0" => {} |
|
b"\\lan\\1" => { |
|
flags: FilterFlags::LAN, |
|
} |
|
} |
|
parse_bots(flags_mask: FilterFlags::BOTS) { |
|
b"\\bots\\0" => {} |
|
b"\\bots\\1" => { |
|
flags: FilterFlags::BOTS, |
|
} |
|
} |
|
|
|
parse_all { |
|
b"\ |
|
\\bots\\1\ |
|
\\clver\\0.20\ |
|
\\dedicated\\1\ |
|
\\empty\\1\ |
|
\\full\\1\ |
|
\\gamedir\\valve\ |
|
\\lan\\1\ |
|
\\map\\crossfire\ |
|
\\nat\\1\ |
|
\\noplayers\\1\ |
|
\\password\\1\ |
|
\\secure\\1\ |
|
" => { |
|
gamedir: Some(Str(&b"valve"[..])), |
|
map: Some(Str(&b"crossfire"[..])), |
|
clver: Some(Version::new(0, 20)), |
|
flags: FilterFlags::all(), |
|
flags_mask: FilterFlags::all(), |
|
} |
|
} |
|
} |
|
|
|
macro_rules! servers { |
|
($($addr:expr => $info:expr $(=> $func:expr)?)+) => ( |
|
[$({ |
|
let addr = $addr.parse::<SocketAddrV4>().unwrap(); |
|
let mut buf = [0; 512]; |
|
let n = CursorMut::new(&mut buf) |
|
.put_bytes(ServerAdd::HEADER).unwrap() |
|
.put_key("challenge", 0).unwrap() |
|
.put_bytes($info).unwrap() |
|
.pos(); |
|
let p = ServerAdd::<Str<&[u8]>>::decode(&buf[..n]).unwrap(); |
|
let server = ServerInfo::new(&p); |
|
$( |
|
let mut server = server; |
|
let func: fn(&mut Server) = $func; |
|
func(&mut server); |
|
)? |
|
(addr, server) |
|
}),+] |
|
); |
|
} |
|
|
|
macro_rules! matches { |
|
($servers:expr, $filter:expr$(, $expected:expr)*) => ( |
|
let servers = &$servers; |
|
let filter = Filter::try_from($filter as &[u8]).unwrap(); |
|
let iter = servers |
|
.iter() |
|
.enumerate() |
|
.filter(|(_, (addr, server))| filter.matches(*addr, &server)) |
|
.map(|(i, _)| i); |
|
assert_eq!(iter.collect::<Vec<_>>(), [$($expected),*]) |
|
); |
|
} |
|
|
|
#[test] |
|
fn match_dedicated() { |
|
let s = servers! { |
|
"0.0.0.0:0" => b"" |
|
"0.0.0.0:0" => b"\\type\\d" |
|
"0.0.0.0:0" => b"\\type\\p" |
|
"0.0.0.0:0" => b"\\type\\l" |
|
}; |
|
matches!(s, b"", 0, 1, 2, 3); |
|
matches!(s, b"\\dedicated\\0", 0, 2, 3); |
|
matches!(s, b"\\dedicated\\1", 1); |
|
} |
|
|
|
#[test] |
|
fn match_password() { |
|
let s = servers! { |
|
"0.0.0.0:0" => b"" |
|
"0.0.0.0:0" => b"\\password\\0" |
|
"0.0.0.0:0" => b"\\password\\1" |
|
}; |
|
matches!(s, b"", 0, 1, 2); |
|
matches!(s, b"\\password\\0", 0, 1); |
|
matches!(s, b"\\password\\1", 2); |
|
} |
|
|
|
#[test] |
|
fn match_not_empty() { |
|
let servers = servers! { |
|
"0.0.0.0:0" => b"\\players\\0\\max\\8" |
|
"0.0.0.0:0" => b"\\players\\4\\max\\8" |
|
"0.0.0.0:0" => b"\\players\\8\\max\\8" |
|
}; |
|
matches!(servers, b"", 0, 1, 2); |
|
matches!(servers, b"\\empty\\0", 0); |
|
matches!(servers, b"\\empty\\1", 1, 2); |
|
} |
|
|
|
#[test] |
|
fn match_full() { |
|
let servers = servers! { |
|
"0.0.0.0:0" => b"\\players\\0\\max\\8" |
|
"0.0.0.0:0" => b"\\players\\4\\max\\8" |
|
"0.0.0.0:0" => b"\\players\\8\\max\\8" |
|
}; |
|
matches!(servers, b"", 0, 1, 2); |
|
matches!(servers, b"\\full\\0", 0, 1); |
|
matches!(servers, b"\\full\\1", 2); |
|
} |
|
|
|
#[test] |
|
fn match_noplayers() { |
|
let servers = servers! { |
|
"0.0.0.0:0" => b"\\players\\0\\max\\8" |
|
"0.0.0.0:0" => b"\\players\\4\\max\\8" |
|
"0.0.0.0:0" => b"\\players\\8\\max\\8" |
|
}; |
|
matches!(servers, b"", 0, 1, 2); |
|
matches!(servers, b"\\noplayers\\0", 1, 2); |
|
matches!(servers, b"\\noplayers\\1", 0); |
|
} |
|
|
|
#[test] |
|
fn match_nat() { |
|
let servers = servers! { |
|
"0.0.0.0:0" => b"" |
|
"0.0.0.0:0" => b"\\nat\\0" |
|
"0.0.0.0:0" => b"\\nat\\1" |
|
}; |
|
matches!(servers, b"", 0, 1, 2); |
|
matches!(servers, b"\\nat\\0", 0, 1); |
|
matches!(servers, b"\\nat\\1", 2); |
|
} |
|
|
|
#[test] |
|
fn match_lan() { |
|
let servers = servers! { |
|
"0.0.0.0:0" => b"" |
|
"0.0.0.0:0" => b"\\lan\\0" |
|
"0.0.0.0:0" => b"\\lan\\1" |
|
}; |
|
matches!(servers, b"", 0, 1, 2); |
|
matches!(servers, b"\\lan\\0", 0, 1); |
|
matches!(servers, b"\\lan\\1", 2); |
|
} |
|
|
|
#[test] |
|
fn match_bots() { |
|
let servers = servers! { |
|
"0.0.0.0:0" => b"" |
|
"0.0.0.0:0" => b"\\bots\\0" |
|
"0.0.0.0:0" => b"\\bots\\1" |
|
}; |
|
matches!(servers, b"", 0, 1, 2); |
|
matches!(servers, b"\\bots\\0", 0, 1); |
|
matches!(servers, b"\\bots\\1", 2); |
|
} |
|
|
|
#[test] |
|
fn match_gamedir() { |
|
let servers = servers! { |
|
"0.0.0.0:0" => b"\\gamedir\\valve" |
|
"0.0.0.0:0" => b"\\gamedir\\cstrike" |
|
"0.0.0.0:0" => b"\\gamedir\\dod" |
|
"0.0.0.0:0" => b"\\gamedir\\portal" |
|
"0.0.0.0:0" => b"\\gamedir\\left4dead" |
|
}; |
|
matches!(servers, b"", 0, 1, 2, 3, 4); |
|
matches!(servers, b"\\gamedir\\valve", 0); |
|
matches!(servers, b"\\gamedir\\portal", 3); |
|
matches!(servers, b"\\gamedir\\left4dead", 4); |
|
} |
|
|
|
#[test] |
|
fn match_map() { |
|
let servers = servers! { |
|
"0.0.0.0:0" => b"\\map\\crossfire" |
|
"0.0.0.0:0" => b"\\map\\boot_camp" |
|
"0.0.0.0:0" => b"\\map\\de_dust" |
|
"0.0.0.0:0" => b"\\map\\cs_office" |
|
}; |
|
matches!(servers, b"", 0, 1, 2, 3); |
|
matches!(servers, b"\\map\\crossfire", 0); |
|
matches!(servers, b"\\map\\de_dust", 2); |
|
matches!(servers, b"\\map\\cs_office", 3); |
|
} |
|
}
|
|
|