mirror of
https://github.com/tribufu/rust-gamedig
synced 2026-06-01 09:42:41 +00:00
refator: copy cli into mono
This commit is contained in:
parent
66ae3c296e
commit
80f6b87991
63 changed files with 244 additions and 34 deletions
4
crates/lib/src/services/mod.rs
Normal file
4
crates/lib/src/services/mod.rs
Normal 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;
|
||||
7
crates/lib/src/services/valve_master_server/mod.rs
Normal file
7
crates/lib/src/services/valve_master_server/mod.rs
Normal 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::*;
|
||||
155
crates/lib/src/services/valve_master_server/service.rs
Normal file
155
crates/lib/src/services/valve_master_server/service.rs
Normal 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)
|
||||
}
|
||||
234
crates/lib/src/services/valve_master_server/types.rs
Normal file
234
crates/lib/src/services/valve_master_server/types.rs
Normal 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,
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue