Denis Drakhnia
1 year ago
9 changed files with 707 additions and 10 deletions
@ -0,0 +1,14 @@
@@ -0,0 +1,14 @@
|
||||
[package] |
||||
name = "xash3d-query" |
||||
version = "0.1.0" |
||||
license = "GPL-3.0-only" |
||||
authors = ["Denis Drakhnia <numas13@gmail.com>"] |
||||
edition = "2021" |
||||
rust-version = "1.56" |
||||
|
||||
[dependencies] |
||||
thiserror = "1.0.49" |
||||
getopts = "0.2.21" |
||||
serde = { version = "1.0.188", features = ["derive"] } |
||||
serde_json = "1.0.107" |
||||
xash3d-protocol = { path = "../protocol", version = "0.1.0" } |
@ -0,0 +1,166 @@
@@ -0,0 +1,166 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
// SPDX-FileCopyrightText: 2023 Denis Drakhnia <numas13@gmail.com>
|
||||
|
||||
use std::process; |
||||
|
||||
use getopts::Options; |
||||
|
||||
const BIN_NAME: &str = env!("CARGO_BIN_NAME"); |
||||
const PKG_NAME: &str = env!("CARGO_PKG_NAME"); |
||||
const PKG_VERSION: &str = env!("CARGO_PKG_VERSION"); |
||||
|
||||
const DEFAULT_HOST: &str = "mentality.rip"; |
||||
const DEFAULT_PORT: u16 = 27010; |
||||
|
||||
#[derive(Debug)] |
||||
pub struct Cli { |
||||
pub masters: Vec<Box<str>>, |
||||
pub args: Vec<String>, |
||||
pub master_timeout: u32, |
||||
pub server_timeout: u32, |
||||
pub protocol: Vec<u8>, |
||||
pub json: bool, |
||||
pub debug: bool, |
||||
} |
||||
|
||||
impl Default for Cli { |
||||
fn default() -> Cli { |
||||
Cli { |
||||
masters: vec![ |
||||
format!("{}:{}", DEFAULT_HOST, DEFAULT_PORT).into_boxed_str(), |
||||
//format!("{}:{}", DEFAULT_HOST, DEFAULT_PORT + 1).into_boxed_str(),
|
||||
], |
||||
args: Default::default(), |
||||
master_timeout: 2, |
||||
server_timeout: 2, |
||||
protocol: vec![xash3d_protocol::VERSION, xash3d_protocol::VERSION - 1], |
||||
json: false, |
||||
debug: false, |
||||
} |
||||
} |
||||
} |
||||
|
||||
fn print_usage(opts: Options) { |
||||
let brief = format!( |
||||
"\ |
||||
Usage: {} [options] <COMMAND> [ARGS] |
||||
|
||||
COMMANDS: |
||||
all fetch servers from all masters and fetch info for each server |
||||
info hosts... fetch info for each server |
||||
list fetch servers from all masters and print server addresses\ |
||||
", |
||||
BIN_NAME |
||||
); |
||||
print!("{}", opts.usage(&brief)); |
||||
} |
||||
|
||||
fn print_version() { |
||||
println!("{} v{}", PKG_NAME, PKG_VERSION); |
||||
} |
||||
|
||||
pub fn parse() -> Cli { |
||||
let mut cli = Cli::default(); |
||||
|
||||
let args: Vec<_> = std::env::args().collect(); |
||||
let mut opts = Options::new(); |
||||
opts.optflag("h", "help", "print usage help"); |
||||
opts.optflag("v", "version", "print program version"); |
||||
let help = format!( |
||||
"master address to connect [default: {}]", |
||||
cli.masters.join(",") |
||||
); |
||||
opts.optopt("m", "master", &help, "LIST"); |
||||
let help = format!( |
||||
"time to wait results from masters [default: {}]", |
||||
cli.master_timeout |
||||
); |
||||
opts.optopt("T", "master-timeout", &help, "SECONDS"); |
||||
let help = format!( |
||||
"time to wait results from servers [default: {}]", |
||||
cli.server_timeout |
||||
); |
||||
opts.optopt("t", "server-timeout", &help, "SECONDS"); |
||||
let protocols = cli |
||||
.protocol |
||||
.iter() |
||||
.map(|&i| format!("{}", i)) |
||||
.collect::<Vec<_>>() |
||||
.join(","); |
||||
let help = format!("protocol version [default: {}]", protocols); |
||||
opts.optopt("p", "protocol", &help, "VERSION"); |
||||
opts.optflag("j", "json", "output JSON"); |
||||
opts.optflag("d", "debug", "output debug"); |
||||
|
||||
let matches = match opts.parse(&args[1..]) { |
||||
Ok(m) => m, |
||||
Err(e) => { |
||||
eprintln!("{}", e); |
||||
process::exit(1); |
||||
} |
||||
}; |
||||
|
||||
if matches.opt_present("help") { |
||||
print_usage(opts); |
||||
process::exit(0); |
||||
} |
||||
|
||||
if matches.opt_present("version") { |
||||
print_version(); |
||||
process::exit(0); |
||||
} |
||||
|
||||
if let Some(s) = matches.opt_str("master") { |
||||
cli.masters.clear(); |
||||
|
||||
for mut i in s.split(',').map(String::from) { |
||||
if !i.contains(':') { |
||||
i.push_str(":27010"); |
||||
} |
||||
cli.masters.push(i.into_boxed_str()); |
||||
} |
||||
} |
||||
|
||||
match matches.opt_get("master") { |
||||
Ok(Some(t)) => cli.master_timeout = t, |
||||
Ok(None) => {} |
||||
Err(_) => { |
||||
eprintln!("Invalid master-timeout"); |
||||
process::exit(1); |
||||
} |
||||
} |
||||
|
||||
match matches.opt_get("server-timeout") { |
||||
Ok(Some(t)) => cli.server_timeout = t, |
||||
Ok(None) => {} |
||||
Err(_) => { |
||||
eprintln!("Invalid server-timeout"); |
||||
process::exit(1); |
||||
} |
||||
} |
||||
|
||||
if let Some(s) = matches.opt_str("protocol") { |
||||
cli.protocol.clear(); |
||||
|
||||
let mut error = false; |
||||
for i in s.split(',') { |
||||
match i.parse() { |
||||
Ok(i) => cli.protocol.push(i), |
||||
Err(_) => { |
||||
eprintln!("Invalid protocol version: {}", i); |
||||
error = true; |
||||
} |
||||
} |
||||
} |
||||
|
||||
if error { |
||||
process::exit(1); |
||||
} |
||||
} |
||||
|
||||
cli.json = matches.opt_present("json"); |
||||
cli.debug = matches.opt_present("debug"); |
||||
cli.args = matches.free; |
||||
|
||||
cli |
||||
} |
@ -0,0 +1,447 @@
@@ -0,0 +1,447 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
// SPDX-FileCopyrightText: 2023 Denis Drakhnia <numas13@gmail.com>
|
||||
|
||||
mod cli; |
||||
|
||||
use std::cmp; |
||||
use std::collections::{HashMap, HashSet}; |
||||
use std::fmt; |
||||
use std::io; |
||||
use std::net::{Ipv4Addr, SocketAddrV4, UdpSocket}; |
||||
use std::process; |
||||
use std::sync::mpsc; |
||||
use std::thread; |
||||
use std::time::{Duration, Instant}; |
||||
|
||||
use serde::Serialize; |
||||
use thiserror::Error; |
||||
use xash3d_protocol::types::Str; |
||||
use xash3d_protocol::{filter, game, master, server, Error as ProtocolError}; |
||||
|
||||
use crate::cli::Cli; |
||||
|
||||
#[derive(Error, Debug)] |
||||
enum Error { |
||||
#[error("Undefined command")] |
||||
UndefinedCommand, |
||||
#[error(transparent)] |
||||
Protocol(#[from] ProtocolError), |
||||
#[error(transparent)] |
||||
Io(#[from] io::Error), |
||||
} |
||||
|
||||
#[derive(Clone, Debug, Serialize)] |
||||
#[serde(tag = "type")] |
||||
enum ServerResultKind { |
||||
#[serde(rename = "ok")] |
||||
Ok { info: ServerInfo }, |
||||
#[serde(rename = "error")] |
||||
Error { message: String }, |
||||
#[serde(rename = "invalid")] |
||||
Invalid { message: String, response: String }, |
||||
#[serde(rename = "timeout")] |
||||
Timeout, |
||||
#[serde(rename = "protocol")] |
||||
Protocol, |
||||
} |
||||
|
||||
#[derive(Clone, Debug, Serialize)] |
||||
struct ServerResult { |
||||
address: String, |
||||
#[serde(flatten)] |
||||
kind: ServerResultKind, |
||||
} |
||||
|
||||
impl ServerResult { |
||||
fn new(address: String, kind: ServerResultKind) -> Self { |
||||
Self { |
||||
address: address.to_string(), |
||||
kind, |
||||
} |
||||
} |
||||
|
||||
fn ok<T: Into<ServerInfo>>(address: String, info: T) -> Self { |
||||
Self::new(address, ServerResultKind::Ok { info: info.into() }) |
||||
} |
||||
|
||||
fn timeout(address: String) -> Self { |
||||
Self::new(address, ServerResultKind::Timeout) |
||||
} |
||||
|
||||
fn protocol(address: String) -> Self { |
||||
Self::new(address, ServerResultKind::Protocol) |
||||
} |
||||
|
||||
fn error<T>(address: String, message: T) -> Self |
||||
where |
||||
T: fmt::Display, |
||||
{ |
||||
Self::new( |
||||
address, |
||||
ServerResultKind::Error { |
||||
message: message.to_string(), |
||||
}, |
||||
) |
||||
} |
||||
|
||||
fn invalid<T>(address: String, message: T, response: &[u8]) -> Self |
||||
where |
||||
T: fmt::Display, |
||||
{ |
||||
Self::new( |
||||
address, |
||||
ServerResultKind::Invalid { |
||||
message: message.to_string(), |
||||
response: Str(response).to_string(), |
||||
}, |
||||
) |
||||
} |
||||
} |
||||
|
||||
#[derive(Clone, Debug, Serialize)] |
||||
struct ServerInfo { |
||||
pub gamedir: String, |
||||
pub map: String, |
||||
pub host: String, |
||||
pub protocol: u8, |
||||
pub numcl: u8, |
||||
pub maxcl: u8, |
||||
pub dm: bool, |
||||
pub team: bool, |
||||
pub coop: bool, |
||||
pub password: bool, |
||||
} |
||||
|
||||
impl From<server::GetServerInfoResponse<&str>> for ServerInfo { |
||||
fn from(other: server::GetServerInfoResponse<&str>) -> Self { |
||||
Self { |
||||
gamedir: other.gamedir.to_owned(), |
||||
map: other.map.to_owned(), |
||||
host: other.host.to_owned(), |
||||
protocol: other.protocol, |
||||
numcl: other.numcl, |
||||
maxcl: other.maxcl, |
||||
dm: other.dm, |
||||
team: other.team, |
||||
coop: other.coop, |
||||
password: other.password, |
||||
} |
||||
} |
||||
} |
||||
|
||||
#[derive(Clone, Debug, Serialize)] |
||||
struct InfoResult<'a> { |
||||
protocol: &'a [u8], |
||||
master_timeout: u32, |
||||
server_timeout: u32, |
||||
masters: &'a [Box<str>], |
||||
servers: &'a [&'a ServerResult], |
||||
} |
||||
|
||||
#[derive(Clone, Debug, Serialize)] |
||||
struct ListResult<'a> { |
||||
master_timeout: u32, |
||||
masters: &'a [Box<str>], |
||||
servers: &'a [&'a str], |
||||
} |
||||
|
||||
enum Message { |
||||
Servers(Vec<String>), |
||||
ServerResult(ServerResult), |
||||
End, |
||||
} |
||||
|
||||
fn cmp_address(a: &str, b: &str) -> cmp::Ordering { |
||||
match (a.parse::<SocketAddrV4>(), b.parse::<SocketAddrV4>()) { |
||||
(Ok(a), Ok(b)) => a.cmp(&b), |
||||
_ => a.cmp(b), |
||||
} |
||||
} |
||||
|
||||
fn query_servers(host: &str, timeout: Duration, tx: &mpsc::Sender<Message>) -> Result<(), Error> { |
||||
let sock = UdpSocket::bind("0.0.0.0:0")?; |
||||
sock.connect(host)?; |
||||
|
||||
let mut buf = [0; 512]; |
||||
let p = game::QueryServers { |
||||
region: server::Region::RestOfTheWorld, |
||||
last: SocketAddrV4::new(Ipv4Addr::new(0, 0, 0, 0), 0), |
||||
filter: filter::Filter { |
||||
gamedir: b"valve", |
||||
clver: filter::Version::new(0, 20), |
||||
// TODO: filter
|
||||
..Default::default() |
||||
}, |
||||
}; |
||||
let n = p.encode(&mut buf)?; |
||||
sock.send(&buf[..n])?; |
||||
|
||||
let start_time = Instant::now(); |
||||
while let Some(timeout) = timeout.checked_sub(start_time.elapsed()) { |
||||
sock.set_read_timeout(Some(timeout))?; |
||||
let n = match sock.recv(&mut buf) { |
||||
Ok(n) => n, |
||||
Err(e) => match e.kind() { |
||||
io::ErrorKind::AddrInUse | io::ErrorKind::WouldBlock => break, |
||||
_ => Err(e)?, |
||||
}, |
||||
}; |
||||
if let Ok(packet) = master::QueryServersResponse::decode(&buf[..n]) { |
||||
tx.send(Message::Servers( |
||||
packet.iter().map(|i| i.to_string()).collect(), |
||||
)) |
||||
.unwrap(); |
||||
} else { |
||||
eprintln!("Unexpected packet from master {}", host); |
||||
} |
||||
} |
||||
|
||||
Ok(()) |
||||
} |
||||
|
||||
fn get_server_info( |
||||
addr: String, |
||||
versions: &[u8], |
||||
timeout: Duration, |
||||
) -> Result<ServerResult, Error> { |
||||
let sock = UdpSocket::bind("0.0.0.0:0")?; |
||||
sock.connect(&addr)?; |
||||
sock.set_read_timeout(Some(timeout))?; |
||||
|
||||
for &i in versions { |
||||
let p = game::GetServerInfo::new(i); |
||||
let mut buf = [0; 2048]; |
||||
let n = p.encode(&mut buf)?; |
||||
sock.send(&buf[..n])?; |
||||
|
||||
let n = match sock.recv(&mut buf) { |
||||
Ok(n) => n, |
||||
Err(e) => match e.kind() { |
||||
io::ErrorKind::AddrInUse | io::ErrorKind::WouldBlock => { |
||||
return Ok(ServerResult::timeout(addr)); |
||||
} |
||||
_ => Err(e)?, |
||||
}, |
||||
}; |
||||
|
||||
let response = &buf[..n]; |
||||
match server::GetServerInfoResponse::decode(response) { |
||||
Ok(packet) => { |
||||
return Ok(ServerResult::ok(addr, packet)); |
||||
} |
||||
Err(ProtocolError::InvalidProtocolVersion) => { |
||||
// try another protocol version
|
||||
} |
||||
Err(e) => { |
||||
return Ok(ServerResult::invalid(addr, e, response)); |
||||
} |
||||
} |
||||
} |
||||
|
||||
Ok(ServerResult::protocol(addr)) |
||||
} |
||||
|
||||
fn query_server_info(cli: &Cli, servers: &[String]) -> Result<(), Error> { |
||||
let (tx, rx) = mpsc::channel(); |
||||
let mut workers = 0; |
||||
|
||||
if servers.is_empty() { |
||||
for i in cli.masters.iter() { |
||||
let master = i.to_owned(); |
||||
let tx = tx.clone(); |
||||
let timeout = Duration::from_secs(cli.master_timeout as u64); |
||||
thread::spawn(move || { |
||||
if let Err(e) = query_servers(&master, timeout, &tx) { |
||||
eprintln!("master({}) error: {}", master, e); |
||||
} |
||||
tx.send(Message::End).unwrap(); |
||||
}); |
||||
workers += 1; |
||||
} |
||||
} else { |
||||
tx.send(Message::Servers(servers.to_vec())).unwrap(); |
||||
} |
||||
|
||||
let mut servers = HashMap::new(); |
||||
while let Ok(msg) = rx.recv() { |
||||
match msg { |
||||
Message::Servers(list) => { |
||||
for address in list { |
||||
let tx = tx.clone(); |
||||
let timeout = Duration::from_secs(cli.server_timeout as u64); |
||||
let versions = cli.protocol.clone(); |
||||
thread::spawn(move || { |
||||
let result = get_server_info(address.clone(), &versions, timeout) |
||||
.unwrap_or_else(|e| ServerResult::error(address, e)); |
||||
tx.send(Message::ServerResult(result)).unwrap(); |
||||
tx.send(Message::End).unwrap(); |
||||
}); |
||||
workers += 1; |
||||
} |
||||
} |
||||
Message::End => { |
||||
workers -= 1; |
||||
if workers == 0 { |
||||
break; |
||||
} |
||||
} |
||||
Message::ServerResult(result) => { |
||||
servers.insert(result.address.clone(), result); |
||||
} |
||||
} |
||||
} |
||||
|
||||
let mut servers: Vec<_> = servers.values().collect(); |
||||
servers.sort_by(|a, b| cmp_address(&a.address, &b.address)); |
||||
|
||||
if cli.json || cli.debug { |
||||
let result = InfoResult { |
||||
protocol: &cli.protocol, |
||||
master_timeout: cli.master_timeout, |
||||
server_timeout: cli.server_timeout, |
||||
masters: &cli.masters, |
||||
servers: &servers, |
||||
}; |
||||
|
||||
if cli.json { |
||||
println!("{}", serde_json::to_string_pretty(&result).unwrap()); |
||||
} else if cli.debug { |
||||
println!("{:#?}", result); |
||||
} else { |
||||
todo!() |
||||
} |
||||
} else { |
||||
for i in servers { |
||||
println!("server: {}", i.address); |
||||
|
||||
macro_rules! p { |
||||
($($key:ident: $value:expr),+ $(,)?) => { |
||||
$(println!(" {}: \"{}\"", stringify!($key), $value);)+ |
||||
}; |
||||
} |
||||
|
||||
match &i.kind { |
||||
ServerResultKind::Ok { info } => { |
||||
p! { |
||||
type: "ok", |
||||
host: info.host, |
||||
gamedir: info.gamedir, |
||||
map: info.map, |
||||
protocol: info.protocol, |
||||
numcl: info.numcl, |
||||
maxcl: info.maxcl, |
||||
dm: info.dm, |
||||
team: info.team, |
||||
coop: info.coop, |
||||
password: info.password, |
||||
} |
||||
} |
||||
ServerResultKind::Timeout => { |
||||
p! { |
||||
type: "timeout", |
||||
} |
||||
} |
||||
ServerResultKind::Protocol => { |
||||
p! { |
||||
type: "protocol", |
||||
} |
||||
} |
||||
ServerResultKind::Error { message } => { |
||||
p! { |
||||
type: "error", |
||||
message: message, |
||||
} |
||||
} |
||||
ServerResultKind::Invalid { message, response } => { |
||||
p! { |
||||
type: "invalid", |
||||
message: message, |
||||
response: response, |
||||
} |
||||
} |
||||
} |
||||
println!(); |
||||
} |
||||
} |
||||
|
||||
Ok(()) |
||||
} |
||||
|
||||
fn list_servers(cli: &Cli) -> Result<(), Error> { |
||||
let (tx, rx) = mpsc::channel(); |
||||
let mut workers = 0; |
||||
|
||||
for i in cli.masters.iter() { |
||||
let master = i.to_owned(); |
||||
let tx = tx.clone(); |
||||
let timeout = Duration::from_secs(cli.master_timeout as u64); |
||||
thread::spawn(move || { |
||||
if let Err(e) = query_servers(&master, timeout, &tx) { |
||||
eprintln!("master({}) error: {}", master, e); |
||||
} |
||||
tx.send(Message::End).unwrap(); |
||||
}); |
||||
workers += 1; |
||||
} |
||||
|
||||
let mut servers = HashSet::new(); |
||||
while let Ok(msg) = rx.recv() { |
||||
match msg { |
||||
Message::Servers(list) => { |
||||
servers.extend(list); |
||||
} |
||||
Message::End => { |
||||
workers -= 1; |
||||
if workers == 0 { |
||||
break; |
||||
} |
||||
} |
||||
_ => panic!(), |
||||
} |
||||
} |
||||
|
||||
let mut servers: Vec<_> = servers.iter().map(|i| i.as_str()).collect(); |
||||
servers.sort_by(|a, b| cmp_address(a, b)); |
||||
|
||||
if cli.json || cli.debug { |
||||
let result = ListResult { |
||||
master_timeout: cli.master_timeout, |
||||
masters: &cli.masters, |
||||
servers: &servers, |
||||
}; |
||||
|
||||
if cli.json { |
||||
println!("{}", serde_json::to_string_pretty(&result).unwrap()); |
||||
} else if cli.debug { |
||||
println!("{:#?}", result); |
||||
} else { |
||||
todo!() |
||||
} |
||||
} else { |
||||
for i in servers { |
||||
println!("{}", i); |
||||
} |
||||
} |
||||
|
||||
Ok(()) |
||||
} |
||||
|
||||
fn execute(cli: Cli) -> Result<(), Error> { |
||||
match cli.args.get(0).map(|s| s.as_str()).unwrap_or_default() { |
||||
"all" | "" => query_server_info(&cli, &[])?, |
||||
"info" => query_server_info(&cli, &cli.args[1..])?, |
||||
"list" => list_servers(&cli)?, |
||||
_ => return Err(Error::UndefinedCommand), |
||||
} |
||||
|
||||
Ok(()) |
||||
} |
||||
|
||||
fn main() { |
||||
let cli = cli::parse(); |
||||
|
||||
if let Err(e) = execute(cli) { |
||||
eprintln!("error: {}", e); |
||||
process::exit(1); |
||||
} |
||||
} |
Loading…
Reference in new issue