Denis Drakhnia
1 year ago
24 changed files with 1968 additions and 904 deletions
@ -1,28 +1,6 @@ |
|||||||
[package] |
[workspace] |
||||||
name = "hlmaster" |
resolver = "2" |
||||||
version = "0.1.0" |
members = [ |
||||||
license = "GPL-3.0-only" |
"protocol", |
||||||
authors = ["Denis Drakhnia <numas13@gmail.com>"] |
"master", |
||||||
edition = "2021" |
] |
||||||
rust-version = "1.56" |
|
||||||
|
|
||||||
[features] |
|
||||||
default = ["logtime"] |
|
||||||
logtime = ["chrono"] |
|
||||||
|
|
||||||
[dependencies] |
|
||||||
thiserror = "1.0.49" |
|
||||||
getopts = "0.2.21" |
|
||||||
log = "<0.4.19" |
|
||||||
bitflags = "2.4" |
|
||||||
fastrand = "2.0.1" |
|
||||||
serde = { version = "1.0.188", features = ["derive"] } |
|
||||||
toml = "0.5.11" |
|
||||||
|
|
||||||
[dependencies.chrono] |
|
||||||
version = "<0.4.27" |
|
||||||
optional = true |
|
||||||
default-features = false |
|
||||||
features = ["clock"] |
|
||||||
[target.wasm32-unknown-emscripten.dependencies] |
|
||||||
once_cell = { version = "<1.18", optional = true } |
|
||||||
|
@ -0,0 +1,29 @@ |
|||||||
|
[package] |
||||||
|
name = "xash3d-master" |
||||||
|
version = "0.1.0" |
||||||
|
license = "GPL-3.0-only" |
||||||
|
authors = ["Denis Drakhnia <numas13@gmail.com>"] |
||||||
|
edition = "2021" |
||||||
|
rust-version = "1.56" |
||||||
|
|
||||||
|
[features] |
||||||
|
default = ["logtime"] |
||||||
|
logtime = ["chrono"] |
||||||
|
|
||||||
|
[dependencies] |
||||||
|
thiserror = "1.0.49" |
||||||
|
getopts = "0.2.21" |
||||||
|
log = "<0.4.19" |
||||||
|
bitflags = "2.4" |
||||||
|
fastrand = "2.0.1" |
||||||
|
serde = { version = "1.0.188", features = ["derive"] } |
||||||
|
toml = "0.5.11" |
||||||
|
xash3d-protocol = { path = "../protocol", version = "0.1.0" } |
||||||
|
|
||||||
|
[dependencies.chrono] |
||||||
|
version = "<0.4.27" |
||||||
|
optional = true |
||||||
|
default-features = false |
||||||
|
features = ["clock"] |
||||||
|
[target.wasm32-unknown-emscripten.dependencies] |
||||||
|
once_cell = { version = "<1.18", optional = true } |
@ -0,0 +1,287 @@ |
|||||||
|
// SPDX-License-Identifier: GPL-3.0-only
|
||||||
|
// SPDX-FileCopyrightText: 2023 Denis Drakhnia <numas13@gmail.com>
|
||||||
|
|
||||||
|
use std::collections::HashMap; |
||||||
|
use std::io; |
||||||
|
use std::net::{SocketAddr, SocketAddrV4, ToSocketAddrs, UdpSocket}; |
||||||
|
use std::ops::Deref; |
||||||
|
use std::time::Instant; |
||||||
|
|
||||||
|
use fastrand::Rng; |
||||||
|
use log::{error, info, trace, warn}; |
||||||
|
use thiserror::Error; |
||||||
|
use xash3d_protocol::filter::{Filter, Version}; |
||||||
|
use xash3d_protocol::server::Region; |
||||||
|
use xash3d_protocol::ServerInfo; |
||||||
|
use xash3d_protocol::{game, master, server, Error as ProtocolError}; |
||||||
|
|
||||||
|
use crate::config::{self, Config}; |
||||||
|
|
||||||
|
/// The maximum size of UDP packets.
|
||||||
|
const MAX_PACKET_SIZE: usize = 512; |
||||||
|
|
||||||
|
/// How many cleanup calls should be skipped before removing outdated servers.
|
||||||
|
const SERVER_CLEANUP_MAX: usize = 100; |
||||||
|
|
||||||
|
/// How many cleanup calls should be skipped before removing outdated challenges.
|
||||||
|
const CHALLENGE_CLEANUP_MAX: usize = 100; |
||||||
|
|
||||||
|
#[derive(Error, Debug)] |
||||||
|
pub enum Error { |
||||||
|
#[error("Failed to bind server socket: {0}")] |
||||||
|
BindSocket(io::Error), |
||||||
|
#[error(transparent)] |
||||||
|
Protocol(#[from] ProtocolError), |
||||||
|
#[error(transparent)] |
||||||
|
Io(#[from] io::Error), |
||||||
|
} |
||||||
|
|
||||||
|
/// HashMap entry to keep tracking creation time.
|
||||||
|
#[derive(Clone, Debug)] |
||||||
|
struct Entry<T> { |
||||||
|
time: u32, |
||||||
|
value: T, |
||||||
|
} |
||||||
|
|
||||||
|
impl<T> Entry<T> { |
||||||
|
fn new(time: u32, value: T) -> Self { |
||||||
|
Self { time, value } |
||||||
|
} |
||||||
|
|
||||||
|
fn is_valid(&self, now: u32, duration: u32) -> bool { |
||||||
|
(now - self.time) < duration |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl Entry<ServerInfo> { |
||||||
|
fn matches(&self, addr: SocketAddrV4, region: Region, filter: &Filter) -> bool { |
||||||
|
self.region == region && filter.matches(addr, &self.value) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl<T> Deref for Entry<T> { |
||||||
|
type Target = T; |
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target { |
||||||
|
&self.value |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
struct MasterServer { |
||||||
|
sock: UdpSocket, |
||||||
|
challenges: HashMap<SocketAddrV4, Entry<u32>>, |
||||||
|
servers: HashMap<SocketAddrV4, Entry<ServerInfo>>, |
||||||
|
rng: Rng, |
||||||
|
|
||||||
|
start_time: Instant, |
||||||
|
cleanup_challenges: usize, |
||||||
|
cleanup_servers: usize, |
||||||
|
timeout: config::TimeoutConfig, |
||||||
|
|
||||||
|
clver: Version, |
||||||
|
update_title: Box<str>, |
||||||
|
update_map: Box<str>, |
||||||
|
update_addr: SocketAddrV4, |
||||||
|
} |
||||||
|
|
||||||
|
impl MasterServer { |
||||||
|
fn new(cfg: Config) -> Result<Self, Error> { |
||||||
|
let addr = SocketAddr::new(cfg.server.ip, cfg.server.port); |
||||||
|
info!("Listen address: {}", addr); |
||||||
|
let sock = UdpSocket::bind(addr).map_err(Error::BindSocket)?; |
||||||
|
let update_addr = |
||||||
|
cfg.client |
||||||
|
.update_addr |
||||||
|
.unwrap_or_else(|| match sock.local_addr().unwrap() { |
||||||
|
SocketAddr::V4(addr) => addr, |
||||||
|
_ => todo!(), |
||||||
|
}); |
||||||
|
|
||||||
|
Ok(Self { |
||||||
|
sock, |
||||||
|
start_time: Instant::now(), |
||||||
|
challenges: Default::default(), |
||||||
|
servers: Default::default(), |
||||||
|
rng: Rng::new(), |
||||||
|
cleanup_challenges: 0, |
||||||
|
cleanup_servers: 0, |
||||||
|
timeout: cfg.server.timeout, |
||||||
|
clver: cfg.client.version, |
||||||
|
update_title: cfg.client.update_title, |
||||||
|
update_map: cfg.client.update_map, |
||||||
|
update_addr, |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
fn run(&mut self) -> Result<(), Error> { |
||||||
|
let mut buf = [0; MAX_PACKET_SIZE]; |
||||||
|
loop { |
||||||
|
let (n, from) = self.sock.recv_from(&mut buf)?; |
||||||
|
let from = match from { |
||||||
|
SocketAddr::V4(a) => a, |
||||||
|
_ => { |
||||||
|
warn!("{}: Received message from IPv6, unimplemented", from); |
||||||
|
continue; |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
if let Err(e) = self.handle_packet(from, &buf[..n]) { |
||||||
|
error!("{}: {}", from, e); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
fn handle_packet(&mut self, from: SocketAddrV4, src: &[u8]) -> Result<(), Error> { |
||||||
|
if let Ok(p) = server::Packet::decode(src) { |
||||||
|
match p { |
||||||
|
server::Packet::Challenge(p) => { |
||||||
|
trace!("{}: recv {:?}", from, p); |
||||||
|
let master_challenge = self.add_challenge(from); |
||||||
|
let mut buf = [0; MAX_PACKET_SIZE]; |
||||||
|
let p = master::ChallengeResponse::new(master_challenge, p.server_challenge); |
||||||
|
trace!("{}: send {:?}", from, p); |
||||||
|
let n = p.encode(&mut buf)?; |
||||||
|
self.sock.send_to(&buf[..n], from)?; |
||||||
|
self.remove_outdated_challenges(); |
||||||
|
} |
||||||
|
server::Packet::ServerAdd(p) => { |
||||||
|
trace!("{}: recv {:?}", from, p); |
||||||
|
let entry = match self.challenges.get(&from) { |
||||||
|
Some(e) => e, |
||||||
|
None => { |
||||||
|
trace!("{}: Challenge does not exists", from); |
||||||
|
return Ok(()); |
||||||
|
} |
||||||
|
}; |
||||||
|
if !entry.is_valid(self.now(), self.timeout.challenge) { |
||||||
|
return Ok(()); |
||||||
|
} |
||||||
|
if p.challenge != entry.value { |
||||||
|
warn!( |
||||||
|
"{}: Expected challenge {} but received {}", |
||||||
|
from, entry.value, p.challenge |
||||||
|
); |
||||||
|
return Ok(()); |
||||||
|
} |
||||||
|
if self.challenges.remove(&from).is_some() { |
||||||
|
self.add_server(from, ServerInfo::new(&p)); |
||||||
|
} |
||||||
|
self.remove_outdated_servers(); |
||||||
|
} |
||||||
|
_ => { |
||||||
|
trace!("{}: recv {:?}", from, p); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if let Ok(p) = game::Packet::decode(src) { |
||||||
|
match p { |
||||||
|
game::Packet::QueryServers(p) => { |
||||||
|
trace!("{}: recv {:?}", from, p); |
||||||
|
if p.filter.clver < self.clver { |
||||||
|
let iter = std::iter::once(self.update_addr); |
||||||
|
self.send_server_list(from, iter)?; |
||||||
|
} else { |
||||||
|
let now = self.now(); |
||||||
|
let iter = self |
||||||
|
.servers |
||||||
|
.iter() |
||||||
|
.filter(|i| i.1.is_valid(now, self.timeout.server)) |
||||||
|
.filter(|i| i.1.matches(*i.0, p.region, &p.filter)) |
||||||
|
.map(|i| *i.0); |
||||||
|
self.send_server_list(from, iter)?; |
||||||
|
} |
||||||
|
} |
||||||
|
game::Packet::GetServerInfo(p) => { |
||||||
|
trace!("{}: recv {:?}", from, p); |
||||||
|
let p = server::GetServerInfoResponse { |
||||||
|
map: self.update_map.as_ref(), |
||||||
|
host: self.update_title.as_ref(), |
||||||
|
protocol: 49, |
||||||
|
dm: true, |
||||||
|
maxcl: 32, |
||||||
|
gamedir: "valve", |
||||||
|
..Default::default() |
||||||
|
}; |
||||||
|
trace!("{}: send {:?}", from, p); |
||||||
|
let mut buf = [0; MAX_PACKET_SIZE]; |
||||||
|
let n = p.encode(&mut buf)?; |
||||||
|
self.sock.send_to(&buf[..n], from)?; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
Ok(()) |
||||||
|
} |
||||||
|
|
||||||
|
fn now(&self) -> u32 { |
||||||
|
self.start_time.elapsed().as_secs() as u32 |
||||||
|
} |
||||||
|
|
||||||
|
fn add_challenge(&mut self, addr: SocketAddrV4) -> u32 { |
||||||
|
let x = self.rng.u32(..); |
||||||
|
let entry = Entry::new(self.now(), x); |
||||||
|
self.challenges.insert(addr, entry); |
||||||
|
x |
||||||
|
} |
||||||
|
|
||||||
|
fn remove_outdated_challenges(&mut self) { |
||||||
|
if self.cleanup_challenges < CHALLENGE_CLEANUP_MAX { |
||||||
|
self.cleanup_challenges += 1; |
||||||
|
return; |
||||||
|
} |
||||||
|
let now = self.now(); |
||||||
|
let old = self.challenges.len(); |
||||||
|
self.challenges |
||||||
|
.retain(|_, v| v.is_valid(now, self.timeout.challenge)); |
||||||
|
let new = self.challenges.len(); |
||||||
|
if old != new { |
||||||
|
trace!("Removed {} outdated challenges", old - new); |
||||||
|
} |
||||||
|
self.cleanup_challenges = 0; |
||||||
|
} |
||||||
|
|
||||||
|
fn add_server(&mut self, addr: SocketAddrV4, server: ServerInfo) { |
||||||
|
match self.servers.insert(addr, Entry::new(self.now(), server)) { |
||||||
|
Some(_) => trace!("{}: Updated GameServer", addr), |
||||||
|
None => trace!("{}: New GameServer", addr), |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
fn remove_outdated_servers(&mut self) { |
||||||
|
if self.cleanup_servers < SERVER_CLEANUP_MAX { |
||||||
|
self.cleanup_servers += 1; |
||||||
|
return; |
||||||
|
} |
||||||
|
let now = self.now(); |
||||||
|
let old = self.servers.len(); |
||||||
|
self.servers |
||||||
|
.retain(|_, v| v.is_valid(now, self.timeout.server)); |
||||||
|
let new = self.servers.len(); |
||||||
|
if old != new { |
||||||
|
trace!("Removed {} outdated servers", old - new); |
||||||
|
} |
||||||
|
self.cleanup_servers = 0; |
||||||
|
} |
||||||
|
|
||||||
|
fn send_server_list<A, I>(&self, to: A, iter: I) -> Result<(), Error> |
||||||
|
where |
||||||
|
A: ToSocketAddrs, |
||||||
|
I: Iterator<Item = SocketAddrV4>, |
||||||
|
{ |
||||||
|
let mut list = master::QueryServersResponse::new(iter); |
||||||
|
loop { |
||||||
|
let mut buf = [0; MAX_PACKET_SIZE]; |
||||||
|
let (n, is_end) = list.encode(&mut buf)?; |
||||||
|
self.sock.send_to(&buf[..n], &to)?; |
||||||
|
if is_end { |
||||||
|
break; |
||||||
|
} |
||||||
|
} |
||||||
|
Ok(()) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
pub fn run(cfg: Config) -> Result<(), Error> { |
||||||
|
MasterServer::new(cfg)?.run() |
||||||
|
} |
@ -0,0 +1,12 @@ |
|||||||
|
[package] |
||||||
|
name = "xash3d-protocol" |
||||||
|
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" |
||||||
|
log = "<0.4.19" |
||||||
|
bitflags = "2.4" |
@ -0,0 +1,82 @@ |
|||||||
|
// SPDX-License-Identifier: GPL-3.0-only
|
||||||
|
// SPDX-FileCopyrightText: 2023 Denis Drakhnia <numas13@gmail.com>
|
||||||
|
|
||||||
|
use crate::cursor::{Cursor, CursorMut}; |
||||||
|
use crate::types::Str; |
||||||
|
use crate::Error; |
||||||
|
|
||||||
|
pub const HASH_LEN: usize = 64; |
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq)] |
||||||
|
pub struct AdminChallenge; |
||||||
|
|
||||||
|
impl AdminChallenge { |
||||||
|
pub const HEADER: &'static [u8] = b"adminchallenge"; |
||||||
|
|
||||||
|
pub fn decode(src: &[u8]) -> Result<Self, Error> { |
||||||
|
if src == Self::HEADER { |
||||||
|
Ok(Self) |
||||||
|
} else { |
||||||
|
Err(Error::InvalidPacket) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
pub fn encode(&self, buf: &mut [u8]) -> Result<usize, Error> { |
||||||
|
Ok(CursorMut::new(buf).put_bytes(Self::HEADER)?.pos()) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq)] |
||||||
|
pub struct AdminCommand<'a> { |
||||||
|
pub hash: &'a [u8], |
||||||
|
pub command: Str<&'a [u8]>, |
||||||
|
} |
||||||
|
|
||||||
|
impl<'a> AdminCommand<'a> { |
||||||
|
pub const HEADER: &'static [u8] = b"admin"; |
||||||
|
|
||||||
|
pub fn new(hash: &'a [u8], command: &'a str) -> Self { |
||||||
|
Self { |
||||||
|
hash, |
||||||
|
command: Str(command.as_bytes()), |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
pub fn decode(src: &'a [u8]) -> Result<Self, Error> { |
||||||
|
let mut cur = Cursor::new(src); |
||||||
|
cur.expect(Self::HEADER)?; |
||||||
|
let hash = cur.get_bytes(HASH_LEN)?; |
||||||
|
let command = Str(cur.get_bytes(cur.remaining())?); |
||||||
|
cur.expect_empty()?; |
||||||
|
Ok(Self { hash, command }) |
||||||
|
} |
||||||
|
|
||||||
|
pub fn encode(&self, buf: &mut [u8]) -> Result<usize, Error> { |
||||||
|
Ok(CursorMut::new(buf) |
||||||
|
.put_bytes(Self::HEADER)? |
||||||
|
.put_bytes(self.hash)? |
||||||
|
.put_bytes(&self.command)? |
||||||
|
.pos()) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
#[cfg(test)] |
||||||
|
mod tests { |
||||||
|
use super::*; |
||||||
|
|
||||||
|
#[test] |
||||||
|
fn admin_challenge() { |
||||||
|
let p = AdminChallenge; |
||||||
|
let mut buf = [0; 512]; |
||||||
|
let n = p.encode(&mut buf).unwrap(); |
||||||
|
assert_eq!(AdminChallenge::decode(&buf[..n]), Ok(p)); |
||||||
|
} |
||||||
|
|
||||||
|
#[test] |
||||||
|
fn admin_command() { |
||||||
|
let p = AdminCommand::new(&[1; HASH_LEN], "foo bar baz"); |
||||||
|
let mut buf = [0; 512]; |
||||||
|
let n = p.encode(&mut buf).unwrap(); |
||||||
|
assert_eq!(AdminCommand::decode(&buf[..n]), Ok(p)); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,516 @@ |
|||||||
|
// SPDX-License-Identifier: GPL-3.0-only
|
||||||
|
// SPDX-FileCopyrightText: 2023 Denis Drakhnia <numas13@gmail.com>
|
||||||
|
|
||||||
|
use std::fmt; |
||||||
|
use std::io::{self, Write as _}; |
||||||
|
use std::mem; |
||||||
|
use std::slice; |
||||||
|
use std::str; |
||||||
|
|
||||||
|
use super::types::Str; |
||||||
|
use super::Error; |
||||||
|
|
||||||
|
pub trait GetKeyValue<'a>: Sized { |
||||||
|
fn get_key_value(cur: &mut Cursor<'a>) -> Result<Self, Error>; |
||||||
|
} |
||||||
|
|
||||||
|
impl<'a> GetKeyValue<'a> for &'a [u8] { |
||||||
|
fn get_key_value(cur: &mut Cursor<'a>) -> Result<Self, Error> { |
||||||
|
cur.get_key_value_raw() |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl<'a> GetKeyValue<'a> for Str<&'a [u8]> { |
||||||
|
fn get_key_value(cur: &mut Cursor<'a>) -> Result<Self, Error> { |
||||||
|
cur.get_key_value_raw().map(Str) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl<'a> GetKeyValue<'a> for &'a str { |
||||||
|
fn get_key_value(cur: &mut Cursor<'a>) -> Result<Self, Error> { |
||||||
|
let raw = cur.get_key_value_raw()?; |
||||||
|
str::from_utf8(raw).map_err(|_| Error::InvalidString) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl<'a> GetKeyValue<'a> for bool { |
||||||
|
fn get_key_value(cur: &mut Cursor<'a>) -> Result<Self, Error> { |
||||||
|
match cur.get_key_value_raw()? { |
||||||
|
b"0" => Ok(false), |
||||||
|
b"1" => Ok(true), |
||||||
|
_ => Err(Error::InvalidPacket), |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
macro_rules! impl_get_value { |
||||||
|
($($t:ty),+ $(,)?) => { |
||||||
|
$(impl<'a> GetKeyValue<'a> for $t { |
||||||
|
fn get_key_value(cur: &mut Cursor<'a>) -> Result<Self, Error> { |
||||||
|
cur.get_key_value::<&str>()?.parse().map_err(|_| Error::InvalidPacket) |
||||||
|
} |
||||||
|
})+ |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
impl_get_value! { |
||||||
|
u8, |
||||||
|
u16, |
||||||
|
u32, |
||||||
|
u64, |
||||||
|
|
||||||
|
i8, |
||||||
|
i16, |
||||||
|
i32, |
||||||
|
i64, |
||||||
|
} |
||||||
|
|
||||||
|
// TODO: impl GetKeyValue for f32 and f64
|
||||||
|
|
||||||
|
#[derive(Copy, Clone)] |
||||||
|
pub struct Cursor<'a> { |
||||||
|
buffer: &'a [u8], |
||||||
|
} |
||||||
|
|
||||||
|
macro_rules! impl_get { |
||||||
|
($($n:ident: $t:ty = $f:ident),+ $(,)?) => ( |
||||||
|
$(#[inline] |
||||||
|
pub fn $n(&mut self) -> Result<$t, Error> { |
||||||
|
const N: usize = mem::size_of::<$t>(); |
||||||
|
self.get_array::<N>().map(<$t>::$f) |
||||||
|
})+ |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
impl<'a> Cursor<'a> { |
||||||
|
pub fn new(buffer: &'a [u8]) -> Self { |
||||||
|
Self { buffer } |
||||||
|
} |
||||||
|
|
||||||
|
pub fn end(self) -> &'a [u8] { |
||||||
|
self.buffer |
||||||
|
} |
||||||
|
|
||||||
|
#[inline(always)] |
||||||
|
pub fn remaining(&self) -> usize { |
||||||
|
self.buffer.len() |
||||||
|
} |
||||||
|
|
||||||
|
#[inline(always)] |
||||||
|
pub fn has_remaining(&self) -> bool { |
||||||
|
self.remaining() != 0 |
||||||
|
} |
||||||
|
|
||||||
|
pub fn get_bytes(&mut self, count: usize) -> Result<&'a [u8], Error> { |
||||||
|
if count <= self.remaining() { |
||||||
|
let (head, tail) = self.buffer.split_at(count); |
||||||
|
self.buffer = tail; |
||||||
|
Ok(head) |
||||||
|
} else { |
||||||
|
Err(Error::UnexpectedEnd) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
pub fn advance(&mut self, count: usize) -> Result<(), Error> { |
||||||
|
self.get_bytes(count).map(|_| ()) |
||||||
|
} |
||||||
|
|
||||||
|
pub fn get_array<const N: usize>(&mut self) -> Result<[u8; N], Error> { |
||||||
|
self.get_bytes(N).map(|s| { |
||||||
|
let mut array = [0; N]; |
||||||
|
array.copy_from_slice(s); |
||||||
|
array |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
pub fn get_str(&mut self, n: usize) -> Result<&'a str, Error> { |
||||||
|
let mut cur = *self; |
||||||
|
let s = cur |
||||||
|
.get_bytes(n) |
||||||
|
.and_then(|s| str::from_utf8(s).map_err(|_| Error::InvalidString))?; |
||||||
|
*self = cur; |
||||||
|
Ok(s) |
||||||
|
} |
||||||
|
|
||||||
|
pub fn get_cstr(&mut self) -> Result<Str<&'a [u8]>, Error> { |
||||||
|
let pos = self |
||||||
|
.buffer |
||||||
|
.iter() |
||||||
|
.position(|&c| c == b'\0') |
||||||
|
.ok_or(Error::UnexpectedEnd)?; |
||||||
|
let (head, tail) = self.buffer.split_at(pos); |
||||||
|
self.buffer = &tail[1..]; |
||||||
|
Ok(Str(&head[..pos])) |
||||||
|
} |
||||||
|
|
||||||
|
pub fn get_cstr_as_str(&mut self) -> Result<&'a str, Error> { |
||||||
|
str::from_utf8(&self.get_cstr()?).map_err(|_| Error::InvalidString) |
||||||
|
} |
||||||
|
|
||||||
|
#[inline(always)] |
||||||
|
pub fn get_u8(&mut self) -> Result<u8, Error> { |
||||||
|
self.get_array::<1>().map(|s| s[0]) |
||||||
|
} |
||||||
|
|
||||||
|
#[inline(always)] |
||||||
|
pub fn get_i8(&mut self) -> Result<i8, Error> { |
||||||
|
self.get_array::<1>().map(|s| s[0] as i8) |
||||||
|
} |
||||||
|
|
||||||
|
impl_get! { |
||||||
|
get_u16_le: u16 = from_le_bytes, |
||||||
|
get_u32_le: u32 = from_le_bytes, |
||||||
|
get_u64_le: u64 = from_le_bytes, |
||||||
|
get_i16_le: i16 = from_le_bytes, |
||||||
|
get_i32_le: i32 = from_le_bytes, |
||||||
|
get_i64_le: i64 = from_le_bytes, |
||||||
|
get_f32_le: f32 = from_le_bytes, |
||||||
|
get_f64_le: f64 = from_le_bytes, |
||||||
|
|
||||||
|
get_u16_be: u16 = from_be_bytes, |
||||||
|
get_u32_be: u32 = from_be_bytes, |
||||||
|
get_u64_be: u64 = from_be_bytes, |
||||||
|
get_i16_be: i16 = from_be_bytes, |
||||||
|
get_i32_be: i32 = from_be_bytes, |
||||||
|
get_i64_be: i64 = from_be_bytes, |
||||||
|
get_f32_be: f32 = from_be_bytes, |
||||||
|
get_f64_be: f64 = from_be_bytes, |
||||||
|
|
||||||
|
get_u16_ne: u16 = from_ne_bytes, |
||||||
|
get_u32_ne: u32 = from_ne_bytes, |
||||||
|
get_u64_ne: u64 = from_ne_bytes, |
||||||
|
get_i16_ne: i16 = from_ne_bytes, |
||||||
|
get_i32_ne: i32 = from_ne_bytes, |
||||||
|
get_i64_ne: i64 = from_ne_bytes, |
||||||
|
get_f32_ne: f32 = from_ne_bytes, |
||||||
|
get_f64_ne: f64 = from_ne_bytes, |
||||||
|
} |
||||||
|
|
||||||
|
pub fn expect(&mut self, s: &[u8]) -> Result<(), Error> { |
||||||
|
if self.buffer.starts_with(s) { |
||||||
|
self.advance(s.len())?; |
||||||
|
Ok(()) |
||||||
|
} else { |
||||||
|
Err(Error::InvalidPacket) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
pub fn expect_empty(&self) -> Result<(), Error> { |
||||||
|
if self.has_remaining() { |
||||||
|
Err(Error::InvalidPacket) |
||||||
|
} else { |
||||||
|
Ok(()) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
pub fn take_while<F>(&mut self, mut cond: F) -> Result<&'a [u8], Error> |
||||||
|
where |
||||||
|
F: FnMut(u8) -> bool, |
||||||
|
{ |
||||||
|
self.buffer |
||||||
|
.iter() |
||||||
|
.position(|&i| !cond(i)) |
||||||
|
.ok_or(Error::UnexpectedEnd) |
||||||
|
.and_then(|n| self.get_bytes(n)) |
||||||
|
} |
||||||
|
|
||||||
|
pub fn take_while_or_all<F>(&mut self, cond: F) -> &'a [u8] |
||||||
|
where |
||||||
|
F: FnMut(u8) -> bool, |
||||||
|
{ |
||||||
|
self.take_while(cond).unwrap_or_else(|_| { |
||||||
|
let (head, tail) = self.buffer.split_at(self.buffer.len()); |
||||||
|
self.buffer = tail; |
||||||
|
head |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
pub fn get_key_value_raw(&mut self) -> Result<&'a [u8], Error> { |
||||||
|
let mut cur = *self; |
||||||
|
if cur.get_u8()? == b'\\' { |
||||||
|
let value = cur.take_while_or_all(|c| c != b'\\' && c != b'\n'); |
||||||
|
*self = cur; |
||||||
|
Ok(value) |
||||||
|
} else { |
||||||
|
Err(Error::InvalidPacket) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
pub fn get_key_value<T: GetKeyValue<'a>>(&mut self) -> Result<T, Error> { |
||||||
|
T::get_key_value(self) |
||||||
|
} |
||||||
|
|
||||||
|
pub fn get_key_raw(&mut self) -> Result<&'a [u8], Error> { |
||||||
|
let mut cur = *self; |
||||||
|
if cur.get_u8()? == b'\\' { |
||||||
|
let value = cur.take_while(|c| c != b'\\' && c != b'\n')?; |
||||||
|
*self = cur; |
||||||
|
Ok(value) |
||||||
|
} else { |
||||||
|
Err(Error::InvalidPacket) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
pub fn get_key<T: GetKeyValue<'a>>(&mut self) -> Result<(&'a [u8], T), Error> { |
||||||
|
Ok((self.get_key_raw()?, self.get_key_value()?)) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
pub trait PutKeyValue { |
||||||
|
fn put_key_value<'a, 'b>( |
||||||
|
&self, |
||||||
|
cur: &'b mut CursorMut<'a>, |
||||||
|
) -> Result<&'b mut CursorMut<'a>, Error>; |
||||||
|
} |
||||||
|
|
||||||
|
impl PutKeyValue for &str { |
||||||
|
fn put_key_value<'a, 'b>( |
||||||
|
&self, |
||||||
|
cur: &'b mut CursorMut<'a>, |
||||||
|
) -> Result<&'b mut CursorMut<'a>, Error> { |
||||||
|
cur.put_str(self) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl PutKeyValue for bool { |
||||||
|
fn put_key_value<'a, 'b>( |
||||||
|
&self, |
||||||
|
cur: &'b mut CursorMut<'a>, |
||||||
|
) -> Result<&'b mut CursorMut<'a>, Error> { |
||||||
|
cur.put_u8(if *self { b'1' } else { b'0' }) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
macro_rules! impl_put_key_value { |
||||||
|
($($t:ty),+ $(,)?) => { |
||||||
|
$(impl PutKeyValue for $t { |
||||||
|
fn put_key_value<'a, 'b>(&self, cur: &'b mut CursorMut<'a>) -> Result<&'b mut CursorMut<'a>, Error> { |
||||||
|
cur.put_as_str(self) |
||||||
|
} |
||||||
|
})+ |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
impl_put_key_value! { |
||||||
|
u8, |
||||||
|
u16, |
||||||
|
u32, |
||||||
|
u64, |
||||||
|
|
||||||
|
i8, |
||||||
|
i16, |
||||||
|
i32, |
||||||
|
i64, |
||||||
|
|
||||||
|
f32, |
||||||
|
f64, |
||||||
|
} |
||||||
|
|
||||||
|
pub struct CursorMut<'a> { |
||||||
|
buffer: &'a [u8], |
||||||
|
buffer_mut: &'a mut [u8], |
||||||
|
} |
||||||
|
|
||||||
|
macro_rules! impl_put { |
||||||
|
($($n:ident: $t:ty = $f:ident),+ $(,)?) => ( |
||||||
|
$(#[inline] |
||||||
|
pub fn $n(&mut self, n: $t) -> Result<&mut Self, Error> { |
||||||
|
self.put_array(&n.$f()) |
||||||
|
})+ |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
impl<'a> CursorMut<'a> { |
||||||
|
pub fn new(buffer: &'a mut [u8]) -> Self { |
||||||
|
Self { |
||||||
|
buffer: unsafe { slice::from_raw_parts(buffer.as_ptr(), 0) }, |
||||||
|
buffer_mut: buffer, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
pub fn buffer(&self) -> &'a [u8] { |
||||||
|
self.buffer |
||||||
|
} |
||||||
|
|
||||||
|
pub fn buffer_mut<'b: 'a>(&'b mut self) -> &'a mut [u8] { |
||||||
|
self.buffer_mut |
||||||
|
} |
||||||
|
|
||||||
|
pub fn end(self) -> (&'a [u8], &'a mut [u8]) { |
||||||
|
(self.buffer, self.buffer_mut) |
||||||
|
} |
||||||
|
|
||||||
|
pub fn pos(&mut self) -> usize { |
||||||
|
self.buffer.len() |
||||||
|
} |
||||||
|
|
||||||
|
#[inline(always)] |
||||||
|
pub fn remaining(&self) -> usize { |
||||||
|
self.buffer_mut.len() |
||||||
|
} |
||||||
|
|
||||||
|
pub fn advance<F>(&mut self, count: usize, mut f: F) -> Result<&mut Self, Error> |
||||||
|
where |
||||||
|
F: FnMut(&'a mut [u8]), |
||||||
|
{ |
||||||
|
if count <= self.remaining() { |
||||||
|
let buffer_mut = mem::take(&mut self.buffer_mut); |
||||||
|
let (head, tail) = buffer_mut.split_at_mut(count); |
||||||
|
f(head); |
||||||
|
self.buffer = |
||||||
|
unsafe { slice::from_raw_parts(self.buffer.as_ptr(), self.buffer.len() + count) }; |
||||||
|
self.buffer_mut = tail; |
||||||
|
Ok(self) |
||||||
|
} else { |
||||||
|
Err(Error::UnexpectedEnd) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
pub fn put_bytes(&mut self, s: &[u8]) -> Result<&mut Self, Error> { |
||||||
|
self.advance(s.len(), |i| { |
||||||
|
i.copy_from_slice(s); |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
pub fn put_array<const N: usize>(&mut self, s: &[u8; N]) -> Result<&mut Self, Error> { |
||||||
|
self.advance(N, |i| { |
||||||
|
i.copy_from_slice(s); |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
pub fn put_str(&mut self, s: &str) -> Result<&mut Self, Error> { |
||||||
|
self.put_bytes(s.as_bytes()) |
||||||
|
} |
||||||
|
|
||||||
|
pub fn put_cstr(&mut self, s: &str) -> Result<&mut Self, Error> { |
||||||
|
self.put_str(s)?.put_u8(0) |
||||||
|
} |
||||||
|
|
||||||
|
#[inline(always)] |
||||||
|
pub fn put_u8(&mut self, n: u8) -> Result<&mut Self, Error> { |
||||||
|
self.put_array(&[n]) |
||||||
|
} |
||||||
|
|
||||||
|
#[inline(always)] |
||||||
|
pub fn put_i8(&mut self, n: i8) -> Result<&mut Self, Error> { |
||||||
|
self.put_u8(n as u8) |
||||||
|
} |
||||||
|
|
||||||
|
impl_put! { |
||||||
|
put_u16_le: u16 = to_le_bytes, |
||||||
|
put_u32_le: u32 = to_le_bytes, |
||||||
|
put_u64_le: u64 = to_le_bytes, |
||||||
|
put_i16_le: i16 = to_le_bytes, |
||||||
|
put_i32_le: i32 = to_le_bytes, |
||||||
|
put_i64_le: i64 = to_le_bytes, |
||||||
|
put_f32_le: f32 = to_le_bytes, |
||||||
|
put_f64_le: f64 = to_le_bytes, |
||||||
|
|
||||||
|
put_u16_be: u16 = to_be_bytes, |
||||||
|
put_u32_be: u32 = to_be_bytes, |
||||||
|
put_u64_be: u64 = to_be_bytes, |
||||||
|
put_i16_be: i16 = to_be_bytes, |
||||||
|
put_i32_be: i32 = to_be_bytes, |
||||||
|
put_i64_be: i64 = to_be_bytes, |
||||||
|
put_f32_be: f32 = to_be_bytes, |
||||||
|
put_f64_be: f64 = to_be_bytes, |
||||||
|
|
||||||
|
put_u16_ne: u16 = to_ne_bytes, |
||||||
|
put_u32_ne: u32 = to_ne_bytes, |
||||||
|
put_u64_ne: u64 = to_ne_bytes, |
||||||
|
put_i16_ne: i16 = to_ne_bytes, |
||||||
|
put_i32_ne: i32 = to_ne_bytes, |
||||||
|
put_i64_ne: i64 = to_ne_bytes, |
||||||
|
put_f32_ne: f32 = to_ne_bytes, |
||||||
|
put_f64_ne: f64 = to_ne_bytes, |
||||||
|
} |
||||||
|
|
||||||
|
pub fn put_as_str<T: fmt::Display>(&mut self, value: T) -> Result<&mut Self, Error> { |
||||||
|
let mut cur = io::Cursor::new(mem::take(&mut self.buffer_mut)); |
||||||
|
write!(&mut cur, "{}", value).map_err(|_| Error::UnexpectedEnd)?; |
||||||
|
let n = cur.position() as usize; |
||||||
|
self.buffer_mut = cur.into_inner(); |
||||||
|
self.advance(n, |_| {}) |
||||||
|
} |
||||||
|
|
||||||
|
pub fn put_key_value<T: PutKeyValue>(&mut self, value: T) -> Result<&mut Self, Error> { |
||||||
|
value.put_key_value(self) |
||||||
|
} |
||||||
|
|
||||||
|
pub fn put_key_raw(&mut self, key: &str, value: &[u8]) -> Result<&mut Self, Error> { |
||||||
|
self.put_u8(b'\\')? |
||||||
|
.put_str(key)? |
||||||
|
.put_u8(b'\\')? |
||||||
|
.put_bytes(value) |
||||||
|
} |
||||||
|
|
||||||
|
pub fn put_key<T: PutKeyValue>(&mut self, key: &str, value: T) -> Result<&mut Self, Error> { |
||||||
|
self.put_u8(b'\\')? |
||||||
|
.put_str(key)? |
||||||
|
.put_u8(b'\\')? |
||||||
|
.put_key_value(value) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
#[cfg(test)] |
||||||
|
mod tests { |
||||||
|
use super::*; |
||||||
|
|
||||||
|
#[test] |
||||||
|
fn cursor() -> Result<(), Error> { |
||||||
|
let mut buf = [0; 64]; |
||||||
|
let s = CursorMut::new(&mut buf) |
||||||
|
.put_bytes(b"12345678")? |
||||||
|
.put_array(b"4321")? |
||||||
|
.put_str("abc")? |
||||||
|
.put_cstr("def")? |
||||||
|
.put_u8(0x7f)? |
||||||
|
.put_i8(-128)? |
||||||
|
.put_u32_le(0x44332211)? |
||||||
|
.buffer(); |
||||||
|
|
||||||
|
let mut cur = Cursor::new(s); |
||||||
|
assert_eq!(cur.get_bytes(8), Ok(&b"12345678"[..])); |
||||||
|
assert_eq!(cur.get_array::<4>(), Ok(*b"4321")); |
||||||
|
assert_eq!(cur.get_str(3), Ok("abc")); |
||||||
|
assert_eq!(cur.get_cstr(), Ok(Str(&b"def"[..]))); |
||||||
|
assert_eq!(cur.get_u8(), Ok(0x7f)); |
||||||
|
assert_eq!(cur.get_i8(), Ok(-128)); |
||||||
|
assert_eq!(cur.get_u32_le(), Ok(0x44332211)); |
||||||
|
assert_eq!(cur.get_u8(), Err(Error::UnexpectedEnd)); |
||||||
|
|
||||||
|
Ok(()) |
||||||
|
} |
||||||
|
|
||||||
|
#[test] |
||||||
|
fn key() -> Result<(), Error> { |
||||||
|
let mut buf = [0; 512]; |
||||||
|
let s = CursorMut::new(&mut buf) |
||||||
|
.put_key("p", 49)? |
||||||
|
.put_key("map", "crossfire")? |
||||||
|
.put_key("dm", true)? |
||||||
|
.put_key("team", false)? |
||||||
|
.put_key("coop", false)? |
||||||
|
.put_key("numcl", 4)? |
||||||
|
.put_key("maxcl", 32)? |
||||||
|
.put_key("gamedir", "valve")? |
||||||
|
.put_key("password", false)? |
||||||
|
.put_key("host", "test")? |
||||||
|
.buffer(); |
||||||
|
|
||||||
|
let mut cur = Cursor::new(s); |
||||||
|
assert_eq!(cur.get_key(), Ok((&b"p"[..], 49_u8))); |
||||||
|
assert_eq!(cur.get_key(), Ok((&b"map"[..], "crossfire"))); |
||||||
|
assert_eq!(cur.get_key(), Ok((&b"dm"[..], true))); |
||||||
|
assert_eq!(cur.get_key(), Ok((&b"team"[..], false))); |
||||||
|
assert_eq!(cur.get_key(), Ok((&b"coop"[..], false))); |
||||||
|
assert_eq!(cur.get_key(), Ok((&b"numcl"[..], 4_u8))); |
||||||
|
assert_eq!(cur.get_key(), Ok((&b"maxcl"[..], 32_u8))); |
||||||
|
assert_eq!(cur.get_key(), Ok((&b"gamedir"[..], "valve"))); |
||||||
|
assert_eq!(cur.get_key(), Ok((&b"password"[..], false))); |
||||||
|
assert_eq!(cur.get_key(), Ok((&b"host"[..], "test"))); |
||||||
|
assert_eq!(cur.get_key::<&[u8]>(), Err(Error::UnexpectedEnd)); |
||||||
|
|
||||||
|
Ok(()) |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,128 @@ |
|||||||
|
// SPDX-License-Identifier: GPL-3.0-only
|
||||||
|
// SPDX-FileCopyrightText: 2023 Denis Drakhnia <numas13@gmail.com>
|
||||||
|
|
||||||
|
use std::net::SocketAddrV4; |
||||||
|
|
||||||
|
use crate::cursor::{Cursor, CursorMut}; |
||||||
|
use crate::filter::Filter; |
||||||
|
use crate::server::Region; |
||||||
|
use crate::Error; |
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq)] |
||||||
|
pub struct QueryServers<'a> { |
||||||
|
pub region: Region, |
||||||
|
pub last: SocketAddrV4, |
||||||
|
pub filter: Filter<'a>, |
||||||
|
} |
||||||
|
|
||||||
|
impl<'a> QueryServers<'a> { |
||||||
|
pub const HEADER: &'static [u8] = b"1"; |
||||||
|
|
||||||
|
pub fn decode(src: &'a [u8]) -> Result<Self, Error> { |
||||||
|
let mut cur = Cursor::new(src); |
||||||
|
cur.expect(Self::HEADER)?; |
||||||
|
let region = cur.get_u8()?.try_into().map_err(|_| Error::InvalidPacket)?; |
||||||
|
let last = cur.get_cstr_as_str()?; |
||||||
|
let filter = cur.get_cstr()?; |
||||||
|
cur.expect_empty()?; |
||||||
|
Ok(Self { |
||||||
|
region, |
||||||
|
last: last.parse().map_err(|_| Error::InvalidPacket)?, |
||||||
|
filter: Filter::from_bytes(&filter)?, |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
pub fn encode(&self, buf: &mut [u8]) -> Result<usize, Error> { |
||||||
|
Ok(CursorMut::new(buf) |
||||||
|
.put_bytes(Self::HEADER)? |
||||||
|
.put_u8(self.region as u8)? |
||||||
|
.put_as_str(self.last)? |
||||||
|
.put_u8(0)? |
||||||
|
.put_as_str(&self.filter)? |
||||||
|
.put_u8(0)? |
||||||
|
.pos()) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq)] |
||||||
|
pub struct GetServerInfo { |
||||||
|
pub protocol: u8, |
||||||
|
} |
||||||
|
|
||||||
|
impl GetServerInfo { |
||||||
|
pub const HEADER: &'static [u8] = b"\xff\xff\xff\xffinfo "; |
||||||
|
|
||||||
|
pub fn new(protocol: u8) -> Self { |
||||||
|
Self { protocol } |
||||||
|
} |
||||||
|
|
||||||
|
pub fn decode(src: &[u8]) -> Result<Self, Error> { |
||||||
|
let mut cur = Cursor::new(src); |
||||||
|
cur.expect(Self::HEADER)?; |
||||||
|
let protocol = cur |
||||||
|
.get_str(cur.remaining())? |
||||||
|
.parse() |
||||||
|
.map_err(|_| Error::InvalidPacket)?; |
||||||
|
Ok(Self { protocol }) |
||||||
|
} |
||||||
|
|
||||||
|
pub fn encode(&self, buf: &mut [u8]) -> Result<usize, Error> { |
||||||
|
Ok(CursorMut::new(buf) |
||||||
|
.put_bytes(Self::HEADER)? |
||||||
|
.put_as_str(self.protocol)? |
||||||
|
.pos()) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq)] |
||||||
|
pub enum Packet<'a> { |
||||||
|
QueryServers(QueryServers<'a>), |
||||||
|
GetServerInfo(GetServerInfo), |
||||||
|
} |
||||||
|
|
||||||
|
impl<'a> Packet<'a> { |
||||||
|
pub fn decode(src: &'a [u8]) -> Result<Self, Error> { |
||||||
|
if let Ok(p) = QueryServers::decode(src) { |
||||||
|
return Ok(Self::QueryServers(p)); |
||||||
|
} |
||||||
|
|
||||||
|
if let Ok(p) = GetServerInfo::decode(src) { |
||||||
|
return Ok(Self::GetServerInfo(p)); |
||||||
|
} |
||||||
|
|
||||||
|
Err(Error::InvalidPacket) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
#[cfg(test)] |
||||||
|
mod tests { |
||||||
|
use super::*; |
||||||
|
use crate::filter::{FilterFlags, Version}; |
||||||
|
use std::net::Ipv4Addr; |
||||||
|
|
||||||
|
#[test] |
||||||
|
fn query_servers() { |
||||||
|
let p = QueryServers { |
||||||
|
region: Region::RestOfTheWorld, |
||||||
|
last: SocketAddrV4::new(Ipv4Addr::new(0, 0, 0, 0), 0), |
||||||
|
filter: Filter { |
||||||
|
gamedir: &b"valve"[..], |
||||||
|
map: &b"crossfire"[..], |
||||||
|
clver: Version::new(0, 20), |
||||||
|
flags: FilterFlags::all(), |
||||||
|
flags_mask: FilterFlags::all(), |
||||||
|
}, |
||||||
|
}; |
||||||
|
let mut buf = [0; 512]; |
||||||
|
let n = p.encode(&mut buf).unwrap(); |
||||||
|
assert_eq!(QueryServers::decode(&buf[..n]), Ok(p)); |
||||||
|
} |
||||||
|
|
||||||
|
#[test] |
||||||
|
fn get_server_info() { |
||||||
|
let p = GetServerInfo::new(49); |
||||||
|
let mut buf = [0; 512]; |
||||||
|
let n = p.encode(&mut buf).unwrap(); |
||||||
|
assert_eq!(GetServerInfo::decode(&buf[..n]), Ok(p)); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,28 @@ |
|||||||
|
// SPDX-License-Identifier: GPL-3.0-only
|
||||||
|
// SPDX-FileCopyrightText: 2023 Denis Drakhnia <numas13@gmail.com>
|
||||||
|
|
||||||
|
mod cursor; |
||||||
|
mod server_info; |
||||||
|
|
||||||
|
pub mod admin; |
||||||
|
pub mod filter; |
||||||
|
pub mod game; |
||||||
|
pub mod master; |
||||||
|
pub mod server; |
||||||
|
pub mod types; |
||||||
|
|
||||||
|
pub use server_info::ServerInfo; |
||||||
|
|
||||||
|
use thiserror::Error; |
||||||
|
|
||||||
|
pub const VERSION: u32 = 49; |
||||||
|
|
||||||
|
#[derive(Error, Debug, PartialEq, Eq)] |
||||||
|
pub enum Error { |
||||||
|
#[error("Invalid packet")] |
||||||
|
InvalidPacket, |
||||||
|
#[error("Invalid UTF-8 string")] |
||||||
|
InvalidString, |
||||||
|
#[error("Unexpected end of buffer")] |
||||||
|
UnexpectedEnd, |
||||||
|
} |
@ -0,0 +1,170 @@ |
|||||||
|
// SPDX-License-Identifier: GPL-3.0-only
|
||||||
|
// SPDX-FileCopyrightText: 2023 Denis Drakhnia <numas13@gmail.com>
|
||||||
|
|
||||||
|
use std::net::{Ipv4Addr, SocketAddrV4}; |
||||||
|
|
||||||
|
use super::cursor::{Cursor, CursorMut}; |
||||||
|
use super::Error; |
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq)] |
||||||
|
pub struct ChallengeResponse { |
||||||
|
pub master_challenge: u32, |
||||||
|
pub server_challenge: u32, |
||||||
|
} |
||||||
|
|
||||||
|
impl ChallengeResponse { |
||||||
|
pub const HEADER: &'static [u8] = b"\xff\xff\xff\xffs\n"; |
||||||
|
|
||||||
|
pub fn new(master_challenge: u32, server_challenge: u32) -> Self { |
||||||
|
Self { |
||||||
|
master_challenge, |
||||||
|
server_challenge, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
pub fn decode(src: &[u8]) -> Result<Self, Error> { |
||||||
|
let mut cur = Cursor::new(src); |
||||||
|
cur.expect(Self::HEADER)?; |
||||||
|
let master_challenge = cur.get_u32_le()?; |
||||||
|
let server_challenge = cur.get_u32_le()?; |
||||||
|
cur.expect_empty()?; |
||||||
|
Ok(Self { |
||||||
|
master_challenge, |
||||||
|
server_challenge, |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
pub fn encode<const N: usize>(&self, buf: &mut [u8; N]) -> Result<usize, Error> { |
||||||
|
Ok(CursorMut::new(buf) |
||||||
|
.put_bytes(Self::HEADER)? |
||||||
|
.put_u32_le(self.master_challenge)? |
||||||
|
.put_u32_le(self.server_challenge)? |
||||||
|
.pos()) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq)] |
||||||
|
pub struct QueryServersResponse<I> { |
||||||
|
inner: I, |
||||||
|
} |
||||||
|
|
||||||
|
impl QueryServersResponse<()> { |
||||||
|
pub const HEADER: &'static [u8] = b"\xff\xff\xff\xfff\n"; |
||||||
|
} |
||||||
|
|
||||||
|
impl<'a> QueryServersResponse<&'a [u8]> { |
||||||
|
pub fn decode(src: &'a [u8]) -> Result<Self, Error> { |
||||||
|
let mut cur = Cursor::new(src); |
||||||
|
cur.expect(QueryServersResponse::HEADER)?; |
||||||
|
if cur.remaining() % 6 != 0 { |
||||||
|
return Err(Error::InvalidPacket); |
||||||
|
} |
||||||
|
let s = cur.get_bytes(cur.remaining())?; |
||||||
|
let inner = if s.ends_with(&[0; 6]) { |
||||||
|
&s[..s.len() - 6] |
||||||
|
} else { |
||||||
|
s |
||||||
|
}; |
||||||
|
Ok(Self { inner }) |
||||||
|
} |
||||||
|
|
||||||
|
pub fn iter(&self) -> impl 'a + Iterator<Item = SocketAddrV4> { |
||||||
|
let mut cur = Cursor::new(self.inner); |
||||||
|
(0..self.inner.len() / 6).map(move |_| { |
||||||
|
let ip = Ipv4Addr::from(cur.get_array().unwrap()); |
||||||
|
let port = cur.get_u16_be().unwrap(); |
||||||
|
SocketAddrV4::new(ip, port) |
||||||
|
}) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl<I> QueryServersResponse<I> |
||||||
|
where |
||||||
|
I: Iterator<Item = SocketAddrV4>, |
||||||
|
{ |
||||||
|
pub fn new(iter: I) -> Self { |
||||||
|
Self { inner: iter } |
||||||
|
} |
||||||
|
|
||||||
|
pub fn encode(&mut self, buf: &mut [u8]) -> Result<(usize, bool), Error> { |
||||||
|
let mut cur = CursorMut::new(buf); |
||||||
|
cur.put_bytes(QueryServersResponse::HEADER)?; |
||||||
|
let mut is_end = false; |
||||||
|
while cur.remaining() >= 12 { |
||||||
|
match self.inner.next() { |
||||||
|
Some(i) => { |
||||||
|
cur.put_array(&i.ip().octets())?.put_u16_be(i.port())?; |
||||||
|
} |
||||||
|
None => { |
||||||
|
is_end = true; |
||||||
|
break; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
Ok((cur.put_array(&[0; 6])?.pos(), is_end)) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq)] |
||||||
|
pub struct AdminChallengeResponse { |
||||||
|
pub challenge: u32, |
||||||
|
} |
||||||
|
|
||||||
|
impl AdminChallengeResponse { |
||||||
|
pub const HEADER: &'static [u8] = b"\xff\xff\xff\xffadminchallenge"; |
||||||
|
|
||||||
|
pub fn new(challenge: u32) -> Self { |
||||||
|
Self { challenge } |
||||||
|
} |
||||||
|
|
||||||
|
pub fn decode(src: &[u8]) -> Result<Self, Error> { |
||||||
|
let mut cur = Cursor::new(src); |
||||||
|
cur.expect(Self::HEADER)?; |
||||||
|
let challenge = cur.get_u32_le()?; |
||||||
|
cur.expect_empty()?; |
||||||
|
Ok(Self { challenge }) |
||||||
|
} |
||||||
|
|
||||||
|
pub fn encode(&self, buf: &mut [u8]) -> Result<usize, Error> { |
||||||
|
Ok(CursorMut::new(buf) |
||||||
|
.put_bytes(Self::HEADER)? |
||||||
|
.put_u32_le(self.challenge)? |
||||||
|
.pos()) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
#[cfg(test)] |
||||||
|
mod tests { |
||||||
|
use super::*; |
||||||
|
|
||||||
|
#[test] |
||||||
|
fn challenge_response() { |
||||||
|
let p = ChallengeResponse::new(0x12345678, 0x87654321); |
||||||
|
let mut buf = [0; 512]; |
||||||
|
let n = p.encode(&mut buf).unwrap(); |
||||||
|
assert_eq!(ChallengeResponse::decode(&buf[..n]), Ok(p)); |
||||||
|
} |
||||||
|
|
||||||
|
#[test] |
||||||
|
fn query_servers_response() { |
||||||
|
let servers: &[SocketAddrV4] = &[ |
||||||
|
"1.2.3.4:27001".parse().unwrap(), |
||||||
|
"1.2.3.4:27002".parse().unwrap(), |
||||||
|
"1.2.3.4:27003".parse().unwrap(), |
||||||
|
"1.2.3.4:27004".parse().unwrap(), |
||||||
|
]; |
||||||
|
let mut p = QueryServersResponse::new(servers.iter().cloned()); |
||||||
|
let mut buf = [0; 512]; |
||||||
|
let (n, _) = p.encode(&mut buf).unwrap(); |
||||||
|
let e = QueryServersResponse::decode(&buf[..n]).unwrap(); |
||||||
|
assert_eq!(e.iter().collect::<Vec<_>>(), servers); |
||||||
|
} |
||||||
|
|
||||||
|
#[test] |
||||||
|
fn admin_challenge_response() { |
||||||
|
let p = AdminChallengeResponse::new(0x12345678); |
||||||
|
let mut buf = [0; 64]; |
||||||
|
let n = p.encode(&mut buf).unwrap(); |
||||||
|
assert_eq!(AdminChallengeResponse::decode(&buf[..n]), Ok(p)); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,506 @@ |
|||||||
|
// SPDX-License-Identifier: GPL-3.0-only
|
||||||
|
// SPDX-FileCopyrightText: 2023 Denis Drakhnia <numas13@gmail.com>
|
||||||
|
|
||||||
|
use std::fmt; |
||||||
|
|
||||||
|
use bitflags::bitflags; |
||||||
|
use log::debug; |
||||||
|
|
||||||
|
use super::cursor::{Cursor, CursorMut, GetKeyValue, PutKeyValue}; |
||||||
|
use super::filter::Version; |
||||||
|
use super::types::Str; |
||||||
|
use super::Error; |
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq)] |
||||||
|
pub struct Challenge { |
||||||
|
pub server_challenge: u32, |
||||||
|
} |
||||||
|
|
||||||
|
impl Challenge { |
||||||
|
pub const HEADER: &'static [u8] = b"q\xff"; |
||||||
|
|
||||||
|
pub fn new(server_challenge: u32) -> Self { |
||||||
|
Self { server_challenge } |
||||||
|
} |
||||||
|
|
||||||
|
pub fn decode(src: &[u8]) -> Result<Self, Error> { |
||||||
|
let mut cur = Cursor::new(src); |
||||||
|
cur.expect(Self::HEADER)?; |
||||||
|
let server_challenge = cur.get_u32_le()?; |
||||||
|
cur.expect_empty()?; |
||||||
|
Ok(Self { server_challenge }) |
||||||
|
} |
||||||
|
|
||||||
|
pub fn encode<const N: usize>(&self, buf: &mut [u8; N]) -> Result<usize, Error> { |
||||||
|
Ok(CursorMut::new(buf) |
||||||
|
.put_bytes(Self::HEADER)? |
||||||
|
.put_u32_le(self.server_challenge)? |
||||||
|
.pos()) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
#[derive(Copy, Clone, Debug, PartialEq, Eq)] |
||||||
|
#[repr(u8)] |
||||||
|
pub enum Os { |
||||||
|
Linux, |
||||||
|
Windows, |
||||||
|
Mac, |
||||||
|
Unknown, |
||||||
|
} |
||||||
|
|
||||||
|
impl Default for Os { |
||||||
|
fn default() -> Os { |
||||||
|
Os::Unknown |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl TryFrom<&[u8]> for Os { |
||||||
|
type Error = Error; |
||||||
|
|
||||||
|
fn try_from(value: &[u8]) -> Result<Self, Self::Error> { |
||||||
|
match value { |
||||||
|
b"l" => Ok(Os::Linux), |
||||||
|
b"w" => Ok(Os::Windows), |
||||||
|
b"m" => Ok(Os::Mac), |
||||||
|
_ => Ok(Os::Unknown), |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl GetKeyValue<'_> for Os { |
||||||
|
fn get_key_value(cur: &mut Cursor) -> Result<Self, Error> { |
||||||
|
cur.get_key_value_raw()?.try_into() |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl PutKeyValue for Os { |
||||||
|
fn put_key_value<'a, 'b>( |
||||||
|
&self, |
||||||
|
cur: &'b mut CursorMut<'a>, |
||||||
|
) -> Result<&'b mut CursorMut<'a>, Error> { |
||||||
|
match self { |
||||||
|
Self::Linux => cur.put_str("l"), |
||||||
|
Self::Windows => cur.put_str("w"), |
||||||
|
Self::Mac => cur.put_str("m"), |
||||||
|
Self::Unknown => cur.put_str("?"), |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl fmt::Display for Os { |
||||||
|
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { |
||||||
|
let s = match self { |
||||||
|
Os::Linux => "Linux", |
||||||
|
Os::Windows => "Windows", |
||||||
|
Os::Mac => "Mac", |
||||||
|
Os::Unknown => "Unknown", |
||||||
|
}; |
||||||
|
write!(fmt, "{}", s) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
#[derive(Copy, Clone, Debug, PartialEq)] |
||||||
|
#[repr(u8)] |
||||||
|
pub enum ServerType { |
||||||
|
Dedicated, |
||||||
|
Local, |
||||||
|
Proxy, |
||||||
|
Unknown, |
||||||
|
} |
||||||
|
|
||||||
|
impl Default for ServerType { |
||||||
|
fn default() -> Self { |
||||||
|
Self::Unknown |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl TryFrom<&[u8]> for ServerType { |
||||||
|
type Error = Error; |
||||||
|
|
||||||
|
fn try_from(value: &[u8]) -> Result<Self, Self::Error> { |
||||||
|
match value { |
||||||
|
b"d" => Ok(Self::Dedicated), |
||||||
|
b"l" => Ok(Self::Local), |
||||||
|
b"p" => Ok(Self::Proxy), |
||||||
|
_ => Ok(Self::Unknown), |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl GetKeyValue<'_> for ServerType { |
||||||
|
fn get_key_value(cur: &mut Cursor) -> Result<Self, Error> { |
||||||
|
cur.get_key_value_raw()?.try_into() |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl PutKeyValue for ServerType { |
||||||
|
fn put_key_value<'a, 'b>( |
||||||
|
&self, |
||||||
|
cur: &'b mut CursorMut<'a>, |
||||||
|
) -> Result<&'b mut CursorMut<'a>, Error> { |
||||||
|
match self { |
||||||
|
Self::Dedicated => cur.put_str("d"), |
||||||
|
Self::Local => cur.put_str("l"), |
||||||
|
Self::Proxy => cur.put_str("p"), |
||||||
|
Self::Unknown => cur.put_str("?"), |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl fmt::Display for ServerType { |
||||||
|
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { |
||||||
|
use ServerType as E; |
||||||
|
|
||||||
|
let s = match self { |
||||||
|
E::Dedicated => "dedicated", |
||||||
|
E::Local => "local", |
||||||
|
E::Proxy => "proxy", |
||||||
|
E::Unknown => "unknown", |
||||||
|
}; |
||||||
|
|
||||||
|
write!(fmt, "{}", s) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
#[derive(Copy, Clone, Debug, PartialEq, Eq)] |
||||||
|
#[repr(u8)] |
||||||
|
pub enum Region { |
||||||
|
USEastCoast = 0x00, |
||||||
|
USWestCoast = 0x01, |
||||||
|
SouthAmerica = 0x02, |
||||||
|
Europe = 0x03, |
||||||
|
Asia = 0x04, |
||||||
|
Australia = 0x05, |
||||||
|
MiddleEast = 0x06, |
||||||
|
Africa = 0x07, |
||||||
|
RestOfTheWorld = 0xff, |
||||||
|
} |
||||||
|
|
||||||
|
impl Default for Region { |
||||||
|
fn default() -> Self { |
||||||
|
Self::RestOfTheWorld |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl TryFrom<u8> for Region { |
||||||
|
type Error = Error; |
||||||
|
|
||||||
|
fn try_from(value: u8) -> Result<Self, Self::Error> { |
||||||
|
match value { |
||||||
|
0x00 => Ok(Region::USEastCoast), |
||||||
|
0x01 => Ok(Region::USWestCoast), |
||||||
|
0x02 => Ok(Region::SouthAmerica), |
||||||
|
0x03 => Ok(Region::Europe), |
||||||
|
0x04 => Ok(Region::Asia), |
||||||
|
0x05 => Ok(Region::Australia), |
||||||
|
0x06 => Ok(Region::MiddleEast), |
||||||
|
0x07 => Ok(Region::Africa), |
||||||
|
0xff => Ok(Region::RestOfTheWorld), |
||||||
|
_ => Err(Error::InvalidPacket), |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl GetKeyValue<'_> for Region { |
||||||
|
fn get_key_value(cur: &mut Cursor) -> Result<Self, Error> { |
||||||
|
cur.get_key_value::<u8>()?.try_into() |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
bitflags! { |
||||||
|
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] |
||||||
|
pub struct ServerFlags: u8 { |
||||||
|
const BOTS = 1 << 0; |
||||||
|
const PASSWORD = 1 << 1; |
||||||
|
const SECURE = 1 << 2; |
||||||
|
const LAN = 1 << 3; |
||||||
|
const NAT = 1 << 4; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Default)] |
||||||
|
pub struct ServerAdd<T> { |
||||||
|
pub gamedir: T, |
||||||
|
pub map: T, |
||||||
|
pub version: Version, |
||||||
|
pub product: T, |
||||||
|
pub challenge: u32, |
||||||
|
pub server_type: ServerType, |
||||||
|
pub os: Os, |
||||||
|
pub region: Region, |
||||||
|
pub protocol: u8, |
||||||
|
pub players: u8, |
||||||
|
pub max: u8, |
||||||
|
pub flags: ServerFlags, |
||||||
|
} |
||||||
|
|
||||||
|
impl ServerAdd<()> { |
||||||
|
pub const HEADER: &'static [u8] = b"0\n"; |
||||||
|
} |
||||||
|
|
||||||
|
impl<'a, T> ServerAdd<T> |
||||||
|
where |
||||||
|
T: 'a + Default + GetKeyValue<'a>, |
||||||
|
{ |
||||||
|
pub fn decode(src: &'a [u8]) -> Result<Self, Error> { |
||||||
|
let mut cur = Cursor::new(src); |
||||||
|
cur.expect(ServerAdd::HEADER)?; |
||||||
|
|
||||||
|
let mut ret = Self::default(); |
||||||
|
let mut challenge = None; |
||||||
|
loop { |
||||||
|
let key = match cur.get_key_raw() { |
||||||
|
Ok(s) => s, |
||||||
|
Err(Error::UnexpectedEnd) => break, |
||||||
|
Err(e) => return Err(e), |
||||||
|
}; |
||||||
|
|
||||||
|
match key { |
||||||
|
b"protocol" => ret.protocol = cur.get_key_value()?, |
||||||
|
b"challenge" => challenge = Some(cur.get_key_value()?), |
||||||
|
b"players" => ret.players = cur.get_key_value()?, |
||||||
|
b"max" => ret.max = cur.get_key_value()?, |
||||||
|
b"gamedir" => ret.gamedir = cur.get_key_value()?, |
||||||
|
b"map" => ret.map = cur.get_key_value()?, |
||||||
|
b"type" => ret.server_type = cur.get_key_value()?, |
||||||
|
b"os" => ret.os = cur.get_key_value()?, |
||||||
|
b"version" => ret.version = cur.get_key_value()?, |
||||||
|
b"region" => ret.region = cur.get_key_value()?, |
||||||
|
b"product" => ret.product = cur.get_key_value()?, |
||||||
|
b"bots" => ret.flags.set(ServerFlags::BOTS, cur.get_key_value()?), |
||||||
|
b"password" => ret.flags.set(ServerFlags::PASSWORD, cur.get_key_value()?), |
||||||
|
b"secure" => ret.flags.set(ServerFlags::SECURE, cur.get_key_value()?), |
||||||
|
b"lan" => ret.flags.set(ServerFlags::LAN, cur.get_key_value()?), |
||||||
|
b"nat" => ret.flags.set(ServerFlags::NAT, cur.get_key_value()?), |
||||||
|
_ => { |
||||||
|
// skip unknown fields
|
||||||
|
let value = cur.get_key_value::<Str<&[u8]>>()?; |
||||||
|
debug!("Invalid ServerInfo field \"{}\" = \"{}\"", Str(key), value); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
match challenge { |
||||||
|
Some(c) => { |
||||||
|
ret.challenge = c; |
||||||
|
Ok(ret) |
||||||
|
} |
||||||
|
None => Err(Error::InvalidPacket), |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl<T> ServerAdd<T> |
||||||
|
where |
||||||
|
T: PutKeyValue + Clone, |
||||||
|
{ |
||||||
|
pub fn encode(&self, buf: &mut [u8]) -> Result<usize, Error> { |
||||||
|
Ok(CursorMut::new(buf) |
||||||
|
.put_bytes(ServerAdd::HEADER)? |
||||||
|
.put_key("protocol", self.protocol)? |
||||||
|
.put_key("challenge", self.challenge)? |
||||||
|
.put_key("players", self.players)? |
||||||
|
.put_key("max", self.max)? |
||||||
|
.put_key("gamedir", self.gamedir.clone())? |
||||||
|
.put_key("map", self.map.clone())? |
||||||
|
.put_key("type", self.server_type)? |
||||||
|
.put_key("os", self.os)? |
||||||
|
.put_key("version", self.version)? |
||||||
|
.put_key("region", self.region as u8)? |
||||||
|
.put_key("product", self.product.clone())? |
||||||
|
.put_key("bots", self.flags.contains(ServerFlags::BOTS))? |
||||||
|
.put_key("password", self.flags.contains(ServerFlags::PASSWORD))? |
||||||
|
.put_key("secure", self.flags.contains(ServerFlags::SECURE))? |
||||||
|
.put_key("lan", self.flags.contains(ServerFlags::LAN))? |
||||||
|
.put_key("nat", self.flags.contains(ServerFlags::NAT))? |
||||||
|
.pos()) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq)] |
||||||
|
pub struct ServerRemove; |
||||||
|
|
||||||
|
impl ServerRemove { |
||||||
|
pub const HEADER: &'static [u8] = b"b\n"; |
||||||
|
|
||||||
|
pub fn decode(src: &[u8]) -> Result<Self, Error> { |
||||||
|
let mut cur = Cursor::new(src); |
||||||
|
cur.expect(Self::HEADER)?; |
||||||
|
cur.expect_empty()?; |
||||||
|
Ok(Self) |
||||||
|
} |
||||||
|
|
||||||
|
pub fn encode<const N: usize>(&self, buf: &mut [u8; N]) -> Result<usize, Error> { |
||||||
|
Ok(CursorMut::new(buf).put_bytes(Self::HEADER)?.pos()) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Default)] |
||||||
|
pub struct GetServerInfoResponse<T> { |
||||||
|
pub gamedir: T, |
||||||
|
pub map: T, |
||||||
|
pub host: T, |
||||||
|
pub protocol: u8, |
||||||
|
pub numcl: u8, |
||||||
|
pub maxcl: u8, |
||||||
|
pub dm: bool, |
||||||
|
pub team: bool, |
||||||
|
pub coop: bool, |
||||||
|
pub password: bool, |
||||||
|
} |
||||||
|
|
||||||
|
impl GetServerInfoResponse<()> { |
||||||
|
pub const HEADER: &'static [u8] = b"\xff\xff\xff\xffinfo\n"; |
||||||
|
} |
||||||
|
|
||||||
|
impl<'a, T> GetServerInfoResponse<T> |
||||||
|
where |
||||||
|
T: 'a + Default + GetKeyValue<'a>, |
||||||
|
{ |
||||||
|
pub fn decode(src: &'a [u8]) -> Result<Self, Error> { |
||||||
|
let mut cur = Cursor::new(src); |
||||||
|
cur.expect(GetServerInfoResponse::HEADER)?; |
||||||
|
|
||||||
|
let mut ret = Self::default(); |
||||||
|
loop { |
||||||
|
let key = match cur.get_key_raw() { |
||||||
|
Ok(s) => s, |
||||||
|
Err(Error::UnexpectedEnd) => break, |
||||||
|
Err(e) => return Err(e), |
||||||
|
}; |
||||||
|
|
||||||
|
match key { |
||||||
|
b"p" => ret.protocol = cur.get_key_value()?, |
||||||
|
b"map" => ret.map = cur.get_key_value()?, |
||||||
|
b"dm" => ret.dm = cur.get_key_value()?, |
||||||
|
b"team" => ret.team = cur.get_key_value()?, |
||||||
|
b"coop" => ret.coop = cur.get_key_value()?, |
||||||
|
b"numcl" => ret.numcl = cur.get_key_value()?, |
||||||
|
b"maxcl" => ret.maxcl = cur.get_key_value()?, |
||||||
|
b"gamedir" => ret.gamedir = cur.get_key_value()?, |
||||||
|
b"password" => ret.password = cur.get_key_value()?, |
||||||
|
b"host" => ret.host = cur.get_key_value()?, |
||||||
|
_ => { |
||||||
|
// skip unknown fields
|
||||||
|
let value = cur.get_key_value::<Str<&[u8]>>()?; |
||||||
|
debug!( |
||||||
|
"Invalid GetServerInfo field \"{}\" = \"{}\"", |
||||||
|
Str(key), |
||||||
|
value |
||||||
|
); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
Ok(ret) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl<'a> GetServerInfoResponse<&'a str> { |
||||||
|
pub fn encode(&self, buf: &mut [u8]) -> Result<usize, Error> { |
||||||
|
Ok(CursorMut::new(buf) |
||||||
|
.put_bytes(GetServerInfoResponse::HEADER)? |
||||||
|
.put_key("p", self.protocol)? |
||||||
|
.put_key("map", self.map)? |
||||||
|
.put_key("dm", self.dm)? |
||||||
|
.put_key("team", self.team)? |
||||||
|
.put_key("coop", self.coop)? |
||||||
|
.put_key("numcl", self.numcl)? |
||||||
|
.put_key("maxcl", self.maxcl)? |
||||||
|
.put_key("gamedir", self.gamedir)? |
||||||
|
.put_key("password", self.password)? |
||||||
|
.put_key("host", self.host)? |
||||||
|
.pos()) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq)] |
||||||
|
pub enum Packet<'a> { |
||||||
|
Challenge(Challenge), |
||||||
|
ServerAdd(ServerAdd<Str<&'a [u8]>>), |
||||||
|
ServerRemove, |
||||||
|
GetServerInfoResponse(GetServerInfoResponse<Str<&'a [u8]>>), |
||||||
|
} |
||||||
|
|
||||||
|
impl<'a> Packet<'a> { |
||||||
|
pub fn decode(src: &'a [u8]) -> Result<Self, Error> { |
||||||
|
if let Ok(p) = Challenge::decode(src) { |
||||||
|
return Ok(Self::Challenge(p)); |
||||||
|
} |
||||||
|
|
||||||
|
if let Ok(p) = ServerAdd::decode(src) { |
||||||
|
return Ok(Self::ServerAdd(p)); |
||||||
|
} |
||||||
|
|
||||||
|
if ServerRemove::decode(src).is_ok() { |
||||||
|
return Ok(Self::ServerRemove); |
||||||
|
} |
||||||
|
|
||||||
|
if let Ok(p) = GetServerInfoResponse::decode(src) { |
||||||
|
return Ok(Self::GetServerInfoResponse(p)); |
||||||
|
} |
||||||
|
|
||||||
|
Err(Error::InvalidPacket) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
#[cfg(test)] |
||||||
|
mod tests { |
||||||
|
use super::*; |
||||||
|
|
||||||
|
#[test] |
||||||
|
fn challenge() { |
||||||
|
let p = Challenge::new(0x12345678); |
||||||
|
let mut buf = [0; 128]; |
||||||
|
let n = p.encode(&mut buf).unwrap(); |
||||||
|
assert_eq!(Challenge::decode(&buf[..n]), Ok(p)); |
||||||
|
} |
||||||
|
|
||||||
|
#[test] |
||||||
|
fn server_add() { |
||||||
|
let p = ServerAdd { |
||||||
|
gamedir: "valve", |
||||||
|
map: "crossfire", |
||||||
|
version: Version::new(0, 20), |
||||||
|
product: "foobar", |
||||||
|
challenge: 0x12345678, |
||||||
|
server_type: ServerType::Dedicated, |
||||||
|
os: Os::Linux, |
||||||
|
region: Region::RestOfTheWorld, |
||||||
|
protocol: 49, |
||||||
|
players: 4, |
||||||
|
max: 32, |
||||||
|
flags: ServerFlags::all(), |
||||||
|
}; |
||||||
|
let mut buf = [0; 512]; |
||||||
|
let n = p.encode(&mut buf).unwrap(); |
||||||
|
assert_eq!(ServerAdd::decode(&buf[..n]), Ok(p)); |
||||||
|
} |
||||||
|
|
||||||
|
#[test] |
||||||
|
fn server_remove() { |
||||||
|
let p = ServerRemove; |
||||||
|
let mut buf = [0; 64]; |
||||||
|
let n = p.encode(&mut buf).unwrap(); |
||||||
|
assert_eq!(ServerRemove::decode(&buf[..n]), Ok(p)); |
||||||
|
} |
||||||
|
|
||||||
|
#[test] |
||||||
|
fn get_server_info_response() { |
||||||
|
let p = GetServerInfoResponse { |
||||||
|
protocol: 49, |
||||||
|
map: "crossfire", |
||||||
|
dm: true, |
||||||
|
team: true, |
||||||
|
coop: true, |
||||||
|
numcl: 4, |
||||||
|
maxcl: 32, |
||||||
|
gamedir: "valve", |
||||||
|
password: true, |
||||||
|
host: "Test", |
||||||
|
}; |
||||||
|
let mut buf = [0; 512]; |
||||||
|
let n = p.encode(&mut buf).unwrap(); |
||||||
|
assert_eq!(GetServerInfoResponse::decode(&buf[..n]), Ok(p)); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,27 @@ |
|||||||
|
// SPDX-License-Identifier: GPL-3.0-only
|
||||||
|
// SPDX-FileCopyrightText: 2023 Denis Drakhnia <numas13@gmail.com>
|
||||||
|
|
||||||
|
use super::filter::{FilterFlags, Version}; |
||||||
|
use super::server::{Region, ServerAdd}; |
||||||
|
use super::types::Str; |
||||||
|
|
||||||
|
#[derive(Clone, Debug)] |
||||||
|
pub struct ServerInfo { |
||||||
|
pub version: Version, |
||||||
|
pub gamedir: Box<[u8]>, |
||||||
|
pub map: Box<[u8]>, |
||||||
|
pub flags: FilterFlags, |
||||||
|
pub region: Region, |
||||||
|
} |
||||||
|
|
||||||
|
impl ServerInfo { |
||||||
|
pub fn new(info: &ServerAdd<Str<&[u8]>>) -> Self { |
||||||
|
Self { |
||||||
|
version: info.version, |
||||||
|
gamedir: info.gamedir.to_vec().into_boxed_slice(), |
||||||
|
map: info.map.to_vec().into_boxed_slice(), |
||||||
|
flags: FilterFlags::from(info), |
||||||
|
region: info.region, |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,51 @@ |
|||||||
|
// SPDX-License-Identifier: GPL-3.0-only
|
||||||
|
// SPDX-FileCopyrightText: 2023 Denis Drakhnia <numas13@gmail.com>
|
||||||
|
|
||||||
|
use std::fmt; |
||||||
|
use std::ops::Deref; |
||||||
|
|
||||||
|
/// Wrapper for slice of bytes with printing the bytes as a string
|
||||||
|
#[derive(Copy, Clone, PartialEq, Eq, Default)] |
||||||
|
pub struct Str<T>(pub T); |
||||||
|
|
||||||
|
impl<T> From<T> for Str<T> { |
||||||
|
fn from(value: T) -> Self { |
||||||
|
Self(value) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl<T> fmt::Debug for Str<T> |
||||||
|
where |
||||||
|
T: AsRef<[u8]>, |
||||||
|
{ |
||||||
|
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { |
||||||
|
for &c in self.0.as_ref() { |
||||||
|
match c { |
||||||
|
b'\n' => write!(fmt, "\\n")?, |
||||||
|
b'\t' => write!(fmt, "\\t")?, |
||||||
|
_ if c.is_ascii_graphic() || c == b' ' => { |
||||||
|
write!(fmt, "{}", c as char)?; |
||||||
|
} |
||||||
|
_ => write!(fmt, "\\x{:02x}", c)?, |
||||||
|
} |
||||||
|
} |
||||||
|
Ok(()) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl<T> fmt::Display for Str<T> |
||||||
|
where |
||||||
|
T: AsRef<[u8]>, |
||||||
|
{ |
||||||
|
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { |
||||||
|
<Self as fmt::Debug>::fmt(self, fmt) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl<T> Deref for Str<T> { |
||||||
|
type Target = T; |
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target { |
||||||
|
&self.0 |
||||||
|
} |
||||||
|
} |
@ -1,91 +0,0 @@ |
|||||||
// SPDX-License-Identifier: GPL-3.0-only
|
|
||||||
// SPDX-FileCopyrightText: 2023 Denis Drakhnia <numas13@gmail.com>
|
|
||||||
|
|
||||||
use std::fmt; |
|
||||||
use std::io; |
|
||||||
use std::ops::Deref; |
|
||||||
use std::str; |
|
||||||
|
|
||||||
use log::debug; |
|
||||||
use thiserror::Error; |
|
||||||
|
|
||||||
use crate::server_info::{Region, ServerInfo}; |
|
||||||
|
|
||||||
#[derive(Error, Debug)] |
|
||||||
pub enum Error { |
|
||||||
#[error("Invalid packet")] |
|
||||||
InvalidPacket, |
|
||||||
#[error(transparent)] |
|
||||||
IoError(#[from] io::Error), |
|
||||||
} |
|
||||||
|
|
||||||
pub struct Filter<'a>(&'a [u8]); |
|
||||||
|
|
||||||
impl fmt::Debug for Filter<'_> { |
|
||||||
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { |
|
||||||
String::from_utf8_lossy(self.0).fmt(fmt) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
impl<'a> Deref for Filter<'a> { |
|
||||||
type Target = [u8]; |
|
||||||
|
|
||||||
fn deref(&self) -> &Self::Target { |
|
||||||
self.0 |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
#[derive(Debug)] |
|
||||||
pub enum Packet<'a> { |
|
||||||
Challenge(Option<u32>), |
|
||||||
ServerAdd(Option<u32>, ServerInfo<&'a str>), |
|
||||||
ServerRemove, |
|
||||||
QueryServers(Region, Filter<'a>), |
|
||||||
ServerInfo, |
|
||||||
} |
|
||||||
|
|
||||||
impl<'a> Packet<'a> { |
|
||||||
pub fn decode(s: &'a [u8]) -> Result<Self, Error> { |
|
||||||
match s { |
|
||||||
[b'1', region, tail @ ..] => { |
|
||||||
let region = Region::try_from(*region).map_err(|_| Error::InvalidPacket)?; |
|
||||||
let (tail, _last_ip) = decode_cstr(tail)?; |
|
||||||
let (tail, filter) = decode_cstr(tail)?; |
|
||||||
if !tail.is_empty() { |
|
||||||
return Err(Error::InvalidPacket); |
|
||||||
} |
|
||||||
|
|
||||||
Ok(Self::QueryServers(region, Filter(filter))) |
|
||||||
} |
|
||||||
[b'q', 0xff, b0, b1, b2, b3] => { |
|
||||||
let challenge = u32::from_le_bytes([*b0, *b1, *b2, *b3]); |
|
||||||
Ok(Self::Challenge(Some(challenge))) |
|
||||||
} |
|
||||||
[b'0', b'\n', tail @ ..] => { |
|
||||||
let (challenge, info, tail) = |
|
||||||
ServerInfo::from_bytes(tail).map_err(|_| Error::InvalidPacket)?; |
|
||||||
if tail != b"" && tail != b"\n" { |
|
||||||
debug!("unexpected end {:?}", tail); |
|
||||||
} |
|
||||||
Ok(Self::ServerAdd(challenge, info)) |
|
||||||
} |
|
||||||
[b'b', b'\n'] => Ok(Self::ServerRemove), |
|
||||||
[b'q'] => Ok(Self::Challenge(None)), |
|
||||||
[0xff, 0xff, 0xff, 0xff, b'i', b'n', b'f', b'o', b' ', _, _] => Ok(Self::ServerInfo), |
|
||||||
_ => Err(Error::InvalidPacket), |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
fn decode_cstr(data: &[u8]) -> Result<(&[u8], &[u8]), Error> { |
|
||||||
data.iter() |
|
||||||
.position(|&c| c == 0) |
|
||||||
.ok_or(Error::InvalidPacket) |
|
||||||
.map(|offset| (&data[offset + 1..], &data[..offset])) |
|
||||||
} |
|
||||||
|
|
||||||
// fn decode_str(data: &[u8]) -> Result<(&[u8], &str), Error> {
|
|
||||||
// let (tail, s) = decode_cstr(data)?;
|
|
||||||
// let s = str::from_utf8(s).map_err(|_| Error::InvalidPacket)?;
|
|
||||||
// Ok((tail, s))
|
|
||||||
// }
|
|
@ -1,342 +0,0 @@ |
|||||||
// SPDX-License-Identifier: GPL-3.0-only
|
|
||||||
// SPDX-FileCopyrightText: 2023 Denis Drakhnia <numas13@gmail.com>
|
|
||||||
|
|
||||||
use std::collections::HashMap; |
|
||||||
use std::io::prelude::*; |
|
||||||
use std::io::{self, Cursor}; |
|
||||||
use std::net::{SocketAddr, SocketAddrV4, ToSocketAddrs, UdpSocket}; |
|
||||||
use std::ops::Deref; |
|
||||||
use std::time::Instant; |
|
||||||
|
|
||||||
use fastrand::Rng; |
|
||||||
use log::{error, info, trace, warn}; |
|
||||||
use thiserror::Error; |
|
||||||
|
|
||||||
use crate::client::Packet; |
|
||||||
use crate::config::{self, Config}; |
|
||||||
use crate::filter::{Filter, Version}; |
|
||||||
use crate::server::Server; |
|
||||||
use crate::server_info::Region; |
|
||||||
|
|
||||||
/// The maximum size of UDP packets.
|
|
||||||
const MAX_PACKET_SIZE: usize = 512; |
|
||||||
|
|
||||||
const CHALLENGE_RESPONSE_HEADER: &[u8] = b"\xff\xff\xff\xffs\n"; |
|
||||||
const SERVER_LIST_HEADER: &[u8] = b"\xff\xff\xff\xfff\n"; |
|
||||||
|
|
||||||
/// How many cleanup calls should be skipped before removing outdated servers.
|
|
||||||
const SERVER_CLEANUP_MAX: usize = 100; |
|
||||||
|
|
||||||
/// How many cleanup calls should be skipped before removing outdated challenges.
|
|
||||||
const CHALLENGE_CLEANUP_MAX: usize = 100; |
|
||||||
|
|
||||||
#[derive(Error, Debug)] |
|
||||||
pub enum Error { |
|
||||||
#[error("Failed to bind server socket: {0}")] |
|
||||||
BindSocket(io::Error), |
|
||||||
#[error("Failed to decode packet: {0}")] |
|
||||||
ClientPacket(#[from] crate::client::Error), |
|
||||||
#[error("Missing challenge in ServerInfo")] |
|
||||||
MissingChallenge, |
|
||||||
#[error(transparent)] |
|
||||||
Io(#[from] io::Error), |
|
||||||
} |
|
||||||
|
|
||||||
/// HashMap entry to keep tracking creation time.
|
|
||||||
struct Entry<T> { |
|
||||||
time: u32, |
|
||||||
value: T, |
|
||||||
} |
|
||||||
|
|
||||||
impl<T> Entry<T> { |
|
||||||
fn new(time: u32, value: T) -> Self { |
|
||||||
Self { time, value } |
|
||||||
} |
|
||||||
|
|
||||||
fn is_valid(&self, now: u32, duration: u32) -> bool { |
|
||||||
(now - self.time) < duration |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
impl Entry<Server> { |
|
||||||
fn matches(&self, addr: SocketAddrV4, region: Region, filter: &Filter) -> bool { |
|
||||||
self.region == region && filter.matches(addr, self) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
impl<T> Deref for Entry<T> { |
|
||||||
type Target = T; |
|
||||||
|
|
||||||
fn deref(&self) -> &Self::Target { |
|
||||||
&self.value |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
struct MasterServer { |
|
||||||
sock: UdpSocket, |
|
||||||
challenges: HashMap<SocketAddrV4, Entry<u32>>, |
|
||||||
servers: HashMap<SocketAddrV4, Entry<Server>>, |
|
||||||
rng: Rng, |
|
||||||
|
|
||||||
start_time: Instant, |
|
||||||
cleanup_challenges: usize, |
|
||||||
cleanup_servers: usize, |
|
||||||
timeout: config::TimeoutConfig, |
|
||||||
|
|
||||||
clver: Version, |
|
||||||
update_title: Box<str>, |
|
||||||
update_map: Box<str>, |
|
||||||
update_addr: SocketAddrV4, |
|
||||||
} |
|
||||||
|
|
||||||
impl MasterServer { |
|
||||||
fn new(cfg: Config) -> Result<Self, Error> { |
|
||||||
let addr = SocketAddr::new(cfg.server.ip, cfg.server.port); |
|
||||||
info!("Listen address: {}", addr); |
|
||||||
let sock = UdpSocket::bind(addr).map_err(Error::BindSocket)?; |
|
||||||
let update_addr = |
|
||||||
cfg.client |
|
||||||
.update_addr |
|
||||||
.unwrap_or_else(|| match sock.local_addr().unwrap() { |
|
||||||
SocketAddr::V4(addr) => addr, |
|
||||||
_ => todo!(), |
|
||||||
}); |
|
||||||
|
|
||||||
Ok(Self { |
|
||||||
sock, |
|
||||||
start_time: Instant::now(), |
|
||||||
challenges: Default::default(), |
|
||||||
servers: Default::default(), |
|
||||||
rng: Rng::new(), |
|
||||||
cleanup_challenges: 0, |
|
||||||
cleanup_servers: 0, |
|
||||||
timeout: cfg.server.timeout, |
|
||||||
clver: cfg.client.version, |
|
||||||
update_title: cfg.client.update_title, |
|
||||||
update_map: cfg.client.update_map, |
|
||||||
update_addr, |
|
||||||
}) |
|
||||||
} |
|
||||||
|
|
||||||
fn run(&mut self) -> Result<(), Error> { |
|
||||||
let mut buf = [0; MAX_PACKET_SIZE]; |
|
||||||
loop { |
|
||||||
let (n, from) = self.sock.recv_from(&mut buf)?; |
|
||||||
let from = match from { |
|
||||||
SocketAddr::V4(a) => a, |
|
||||||
_ => { |
|
||||||
warn!("{}: Received message from IPv6, unimplemented", from); |
|
||||||
continue; |
|
||||||
} |
|
||||||
}; |
|
||||||
|
|
||||||
if let Err(e) = self.handle_packet(from, &buf[..n]) { |
|
||||||
error!("{}: {}", from, e); |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
fn handle_packet(&mut self, from: SocketAddrV4, s: &[u8]) -> Result<(), Error> { |
|
||||||
let packet = match Packet::decode(s) { |
|
||||||
Ok(p) => p, |
|
||||||
Err(_) => { |
|
||||||
trace!("{}: Failed to decode {:?}", from, s); |
|
||||||
return Ok(()); |
|
||||||
} |
|
||||||
}; |
|
||||||
|
|
||||||
trace!("{}: recv {:?}", from, packet); |
|
||||||
|
|
||||||
match packet { |
|
||||||
Packet::Challenge(server_challenge) => { |
|
||||||
let challenge = self.add_challenge(from); |
|
||||||
trace!("{}: New challenge {}", from, challenge); |
|
||||||
self.send_challenge_response(from, challenge, server_challenge)?; |
|
||||||
self.remove_outdated_challenges(); |
|
||||||
} |
|
||||||
Packet::ServerAdd(challenge, info) => { |
|
||||||
let challenge = match challenge { |
|
||||||
Some(c) => c, |
|
||||||
None => return Err(Error::MissingChallenge), |
|
||||||
}; |
|
||||||
let entry = match self.challenges.get(&from) { |
|
||||||
Some(e) => e, |
|
||||||
None => { |
|
||||||
trace!("{}: Challenge does not exists", from); |
|
||||||
return Ok(()); |
|
||||||
} |
|
||||||
}; |
|
||||||
if !entry.is_valid(self.now(), self.timeout.challenge) { |
|
||||||
return Ok(()); |
|
||||||
} |
|
||||||
if challenge != entry.value { |
|
||||||
warn!( |
|
||||||
"{}: Expected challenge {} but received {}", |
|
||||||
from, entry.value, challenge |
|
||||||
); |
|
||||||
return Ok(()); |
|
||||||
} |
|
||||||
if self.challenges.remove(&from).is_some() { |
|
||||||
self.add_server(from, Server::new(&info)); |
|
||||||
} |
|
||||||
self.remove_outdated_servers(); |
|
||||||
} |
|
||||||
Packet::ServerRemove => { /* ignore */ } |
|
||||||
Packet::QueryServers(region, filter) => { |
|
||||||
let filter = match Filter::from_bytes(&filter) { |
|
||||||
Ok(f) => f, |
|
||||||
_ => { |
|
||||||
warn!("{}: Invalid filter: {:?}", from, filter); |
|
||||||
return Ok(()); |
|
||||||
} |
|
||||||
}; |
|
||||||
|
|
||||||
if filter.clver.map_or(true, |v| v < self.clver) { |
|
||||||
let iter = std::iter::once(&self.update_addr); |
|
||||||
self.send_server_list(from, iter)?; |
|
||||||
} else { |
|
||||||
let now = self.now(); |
|
||||||
let iter = self |
|
||||||
.servers |
|
||||||
.iter() |
|
||||||
.filter(|i| i.1.is_valid(now, self.timeout.server)) |
|
||||||
.filter(|i| i.1.matches(*i.0, region, &filter)) |
|
||||||
.map(|i| i.0); |
|
||||||
self.send_server_list(from, iter)?; |
|
||||||
} |
|
||||||
} |
|
||||||
Packet::ServerInfo => { |
|
||||||
let mut buf = [0; MAX_PACKET_SIZE]; |
|
||||||
let mut cur = Cursor::new(&mut buf[..]); |
|
||||||
cur.write_all(b"\xff\xff\xff\xffinfo\n")?; |
|
||||||
cur.write_all(b"\\p\\49")?; |
|
||||||
cur.write_all(b"\\map\\")?; |
|
||||||
cur.write_all(self.update_map.as_bytes())?; |
|
||||||
cur.write_all(b"\\dm\\1")?; |
|
||||||
cur.write_all(b"\\team\\0")?; |
|
||||||
cur.write_all(b"\\coop\\0")?; |
|
||||||
cur.write_all(b"\\numcl\\0")?; |
|
||||||
cur.write_all(b"\\maxcl\\0")?; |
|
||||||
cur.write_all(b"\\gamedir\\valve")?; |
|
||||||
cur.write_all(b"\\password\\0")?; |
|
||||||
cur.write_all(b"\\host\\")?; |
|
||||||
cur.write_all(self.update_title.as_bytes())?; |
|
||||||
let n = cur.position() as usize; |
|
||||||
self.sock.send_to(&buf[..n], from)?; |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
Ok(()) |
|
||||||
} |
|
||||||
|
|
||||||
fn now(&self) -> u32 { |
|
||||||
self.start_time.elapsed().as_secs() as u32 |
|
||||||
} |
|
||||||
|
|
||||||
fn add_challenge(&mut self, addr: SocketAddrV4) -> u32 { |
|
||||||
let x = self.rng.u32(..); |
|
||||||
let entry = Entry::new(self.now(), x); |
|
||||||
self.challenges.insert(addr, entry); |
|
||||||
x |
|
||||||
} |
|
||||||
|
|
||||||
fn remove_outdated_challenges(&mut self) { |
|
||||||
if self.cleanup_challenges < CHALLENGE_CLEANUP_MAX { |
|
||||||
self.cleanup_challenges += 1; |
|
||||||
return; |
|
||||||
} |
|
||||||
let now = self.now(); |
|
||||||
let old = self.challenges.len(); |
|
||||||
self.challenges |
|
||||||
.retain(|_, v| v.is_valid(now, self.timeout.challenge)); |
|
||||||
let new = self.challenges.len(); |
|
||||||
if old != new { |
|
||||||
trace!("Removed {} outdated challenges", old - new); |
|
||||||
} |
|
||||||
self.cleanup_challenges = 0; |
|
||||||
} |
|
||||||
|
|
||||||
fn add_server(&mut self, addr: SocketAddrV4, server: Server) { |
|
||||||
match self.servers.insert(addr, Entry::new(self.now(), server)) { |
|
||||||
Some(_) => trace!("{}: Updated GameServer", addr), |
|
||||||
None => trace!("{}: New GameServer", addr), |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
fn remove_outdated_servers(&mut self) { |
|
||||||
if self.cleanup_servers < SERVER_CLEANUP_MAX { |
|
||||||
self.cleanup_servers += 1; |
|
||||||
return; |
|
||||||
} |
|
||||||
let now = self.now(); |
|
||||||
let old = self.servers.len(); |
|
||||||
self.servers |
|
||||||
.retain(|_, v| v.is_valid(now, self.timeout.server)); |
|
||||||
let new = self.servers.len(); |
|
||||||
if old != new { |
|
||||||
trace!("Removed {} outdated servers", old - new); |
|
||||||
} |
|
||||||
self.cleanup_servers = 0; |
|
||||||
} |
|
||||||
|
|
||||||
fn send_challenge_response<A: ToSocketAddrs>( |
|
||||||
&self, |
|
||||||
to: A, |
|
||||||
challenge: u32, |
|
||||||
server_challenge: Option<u32>, |
|
||||||
) -> Result<(), io::Error> { |
|
||||||
let mut buf = [0; MAX_PACKET_SIZE]; |
|
||||||
let mut cur = Cursor::new(&mut buf[..]); |
|
||||||
|
|
||||||
cur.write_all(CHALLENGE_RESPONSE_HEADER)?; |
|
||||||
cur.write_all(&challenge.to_le_bytes())?; |
|
||||||
if let Some(x) = server_challenge { |
|
||||||
cur.write_all(&x.to_le_bytes())?; |
|
||||||
} |
|
||||||
|
|
||||||
let n = cur.position() as usize; |
|
||||||
self.sock.send_to(&buf[..n], to)?; |
|
||||||
Ok(()) |
|
||||||
} |
|
||||||
|
|
||||||
fn send_server_list<'a, A, I>(&self, to: A, mut iter: I) -> Result<(), io::Error> |
|
||||||
where |
|
||||||
A: ToSocketAddrs, |
|
||||||
I: Iterator<Item = &'a SocketAddrV4>, |
|
||||||
{ |
|
||||||
let mut buf = [0; MAX_PACKET_SIZE]; |
|
||||||
let mut done = false; |
|
||||||
while !done { |
|
||||||
let mut cur = Cursor::new(&mut buf[..]); |
|
||||||
cur.write_all(SERVER_LIST_HEADER)?; |
|
||||||
|
|
||||||
loop { |
|
||||||
match iter.next() { |
|
||||||
Some(i) => { |
|
||||||
cur.write_all(&i.ip().octets()[..])?; |
|
||||||
cur.write_all(&i.port().to_be_bytes())?; |
|
||||||
} |
|
||||||
None => { |
|
||||||
done = true; |
|
||||||
break; |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
if (cur.position() as usize) > (MAX_PACKET_SIZE - 12) { |
|
||||||
break; |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
// terminate list
|
|
||||||
cur.write_all(&[0; 6][..])?; |
|
||||||
|
|
||||||
let n = cur.position() as usize; |
|
||||||
self.sock.send_to(&buf[..n], &to)?; |
|
||||||
} |
|
||||||
Ok(()) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
pub fn run(cfg: Config) -> Result<(), Error> { |
|
||||||
MasterServer::new(cfg)?.run() |
|
||||||
} |
|
@ -1,26 +0,0 @@ |
|||||||
// SPDX-License-Identifier: GPL-3.0-only
|
|
||||||
// SPDX-FileCopyrightText: 2023 Denis Drakhnia <numas13@gmail.com>
|
|
||||||
|
|
||||||
use crate::filter::FilterFlags; |
|
||||||
use crate::server_info::{Region, ServerInfo}; |
|
||||||
|
|
||||||
#[derive(Clone, Debug)] |
|
||||||
pub struct Server { |
|
||||||
pub version: Box<str>, |
|
||||||
pub gamedir: Box<str>, |
|
||||||
pub map: Box<str>, |
|
||||||
pub flags: FilterFlags, |
|
||||||
pub region: Region, |
|
||||||
} |
|
||||||
|
|
||||||
impl Server { |
|
||||||
pub fn new(info: &ServerInfo<&str>) -> Self { |
|
||||||
Self { |
|
||||||
version: info.version.to_string().into_boxed_str(), |
|
||||||
gamedir: info.gamedir.to_string().into_boxed_str(), |
|
||||||
map: info.map.to_string().into_boxed_str(), |
|
||||||
flags: FilterFlags::from(info), |
|
||||||
region: info.region, |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
@ -1,329 +0,0 @@ |
|||||||
// SPDX-License-Identifier: GPL-3.0-only
|
|
||||||
// SPDX-FileCopyrightText: 2023 Denis Drakhnia <numas13@gmail.com>
|
|
||||||
|
|
||||||
use std::fmt; |
|
||||||
|
|
||||||
use bitflags::bitflags; |
|
||||||
use log::{debug, log_enabled, Level}; |
|
||||||
use thiserror::Error; |
|
||||||
|
|
||||||
use crate::parser::{Error as ParserError, ParseValue, Parser}; |
|
||||||
|
|
||||||
#[derive(Copy, Clone, Error, Debug, PartialEq, Eq)] |
|
||||||
pub enum Error { |
|
||||||
#[error("Invalid region")] |
|
||||||
InvalidRegion, |
|
||||||
#[error(transparent)] |
|
||||||
Parser(#[from] ParserError), |
|
||||||
} |
|
||||||
|
|
||||||
pub type Result<T, E = Error> = std::result::Result<T, E>; |
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, PartialEq, Eq)] |
|
||||||
#[repr(u8)] |
|
||||||
pub enum Os { |
|
||||||
Linux, |
|
||||||
Windows, |
|
||||||
Mac, |
|
||||||
Unknown, |
|
||||||
} |
|
||||||
|
|
||||||
impl Default for Os { |
|
||||||
fn default() -> Os { |
|
||||||
Os::Unknown |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
impl ParseValue<'_> for Os { |
|
||||||
type Err = Error; |
|
||||||
|
|
||||||
fn parse(p: &mut Parser) -> Result<Self, Self::Err> { |
|
||||||
match p.parse_bytes()? { |
|
||||||
b"l" => Ok(Os::Linux), |
|
||||||
b"w" => Ok(Os::Windows), |
|
||||||
b"m" => Ok(Os::Mac), |
|
||||||
_ => Ok(Os::Unknown), |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
impl fmt::Display for Os { |
|
||||||
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { |
|
||||||
let s = match self { |
|
||||||
Os::Linux => "Linux", |
|
||||||
Os::Windows => "Windows", |
|
||||||
Os::Mac => "Mac", |
|
||||||
Os::Unknown => "Unknown", |
|
||||||
}; |
|
||||||
write!(fmt, "{}", s) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, PartialEq)] |
|
||||||
#[repr(u8)] |
|
||||||
pub enum ServerType { |
|
||||||
Dedicated, |
|
||||||
Local, |
|
||||||
Proxy, |
|
||||||
Unknown, |
|
||||||
} |
|
||||||
|
|
||||||
impl Default for ServerType { |
|
||||||
fn default() -> Self { |
|
||||||
Self::Unknown |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
impl ParseValue<'_> for ServerType { |
|
||||||
type Err = Error; |
|
||||||
|
|
||||||
fn parse(p: &mut Parser) -> Result<Self, Self::Err> { |
|
||||||
match p.parse_bytes()? { |
|
||||||
b"d" => Ok(Self::Dedicated), |
|
||||||
b"l" => Ok(Self::Local), |
|
||||||
b"p" => Ok(Self::Proxy), |
|
||||||
_ => Ok(Self::Unknown), |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
impl fmt::Display for ServerType { |
|
||||||
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { |
|
||||||
use ServerType as E; |
|
||||||
|
|
||||||
let s = match self { |
|
||||||
E::Dedicated => "dedicated", |
|
||||||
E::Local => "local", |
|
||||||
E::Proxy => "proxy", |
|
||||||
E::Unknown => "unknown", |
|
||||||
}; |
|
||||||
|
|
||||||
write!(fmt, "{}", s) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, PartialEq, Eq)] |
|
||||||
#[repr(u8)] |
|
||||||
pub enum Region { |
|
||||||
USEastCoast = 0x00, |
|
||||||
USWestCoast = 0x01, |
|
||||||
SouthAmerica = 0x02, |
|
||||||
Europe = 0x03, |
|
||||||
Asia = 0x04, |
|
||||||
Australia = 0x05, |
|
||||||
MiddleEast = 0x06, |
|
||||||
Africa = 0x07, |
|
||||||
RestOfTheWorld = 0xff, |
|
||||||
} |
|
||||||
|
|
||||||
impl Default for Region { |
|
||||||
fn default() -> Self { |
|
||||||
Self::RestOfTheWorld |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
impl TryFrom<u8> for Region { |
|
||||||
type Error = (); |
|
||||||
|
|
||||||
fn try_from(value: u8) -> Result<Self, Self::Error> { |
|
||||||
match value { |
|
||||||
0x00 => Ok(Region::USEastCoast), |
|
||||||
0x01 => Ok(Region::USWestCoast), |
|
||||||
0x02 => Ok(Region::SouthAmerica), |
|
||||||
0x03 => Ok(Region::Europe), |
|
||||||
0x04 => Ok(Region::Asia), |
|
||||||
0x05 => Ok(Region::Australia), |
|
||||||
0x06 => Ok(Region::MiddleEast), |
|
||||||
0x07 => Ok(Region::Africa), |
|
||||||
0xff => Ok(Region::RestOfTheWorld), |
|
||||||
_ => Err(()), |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
impl ParseValue<'_> for Region { |
|
||||||
type Err = Error; |
|
||||||
|
|
||||||
fn parse(p: &mut Parser<'_>) -> Result<Self, Self::Err> { |
|
||||||
let value = p.parse::<u8>()?; |
|
||||||
Self::try_from(value).map_err(|_| Error::InvalidRegion) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
bitflags! { |
|
||||||
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] |
|
||||||
pub struct ServerFlags: u8 { |
|
||||||
const BOTS = 1 << 0; |
|
||||||
const PASSWORD = 1 << 1; |
|
||||||
const SECURE = 1 << 2; |
|
||||||
const LAN = 1 << 3; |
|
||||||
const NAT = 1 << 4; |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
#[derive(Clone, Debug, Default, PartialEq)] |
|
||||||
pub struct ServerInfo<T = Box<str>> { |
|
||||||
pub gamedir: T, |
|
||||||
pub map: T, |
|
||||||
pub version: T, |
|
||||||
pub product: T, |
|
||||||
pub server_type: ServerType, |
|
||||||
pub os: Os, |
|
||||||
pub region: Region, |
|
||||||
pub protocol: u8, |
|
||||||
pub players: u8, |
|
||||||
pub max: u8, |
|
||||||
pub flags: ServerFlags, |
|
||||||
} |
|
||||||
|
|
||||||
impl<'a, T> ServerInfo<T> |
|
||||||
where |
|
||||||
T: 'a + Default + ParseValue<'a, Err = ParserError>, |
|
||||||
{ |
|
||||||
pub fn from_bytes(src: &'a [u8]) -> Result<(Option<u32>, Self, &'a [u8]), Error> { |
|
||||||
let mut parser = Parser::new(src); |
|
||||||
let (challenge, info) = parser.parse()?; |
|
||||||
let tail = match parser.end() { |
|
||||||
[b'\n', tail @ ..] => tail, |
|
||||||
tail => tail, |
|
||||||
}; |
|
||||||
Ok((challenge, info, tail)) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
impl<'a, T> ParseValue<'a> for (Option<u32>, ServerInfo<T>) |
|
||||||
where |
|
||||||
T: 'a + Default + ParseValue<'a, Err = ParserError>, |
|
||||||
{ |
|
||||||
type Err = Error; |
|
||||||
|
|
||||||
fn parse(p: &mut Parser<'a>) -> Result<Self, Self::Err> { |
|
||||||
let mut info = ServerInfo::default(); |
|
||||||
let mut challenge = None; |
|
||||||
|
|
||||||
loop { |
|
||||||
let name = match p.parse_bytes() { |
|
||||||
Ok(s) => s, |
|
||||||
Err(ParserError::End) => break, |
|
||||||
Err(e) => return Err(e.into()), |
|
||||||
}; |
|
||||||
|
|
||||||
match name { |
|
||||||
b"protocol" => info.protocol = p.parse()?, |
|
||||||
b"challenge" => challenge = Some(p.parse()?), |
|
||||||
b"players" => info.players = p.parse()?, |
|
||||||
b"max" => info.max = p.parse()?, |
|
||||||
b"gamedir" => info.gamedir = p.parse()?, |
|
||||||
b"map" => info.map = p.parse()?, |
|
||||||
b"type" => info.server_type = p.parse()?, |
|
||||||
b"os" => info.os = p.parse()?, |
|
||||||
b"version" => info.version = p.parse()?, |
|
||||||
b"region" => info.region = p.parse()?, |
|
||||||
b"product" => info.product = p.parse()?, |
|
||||||
b"bots" => info.flags.set(ServerFlags::BOTS, p.parse()?), |
|
||||||
b"password" => info.flags.set(ServerFlags::PASSWORD, p.parse()?), |
|
||||||
b"secure" => info.flags.set(ServerFlags::SECURE, p.parse()?), |
|
||||||
b"lan" => info.flags.set(ServerFlags::LAN, p.parse()?), |
|
||||||
b"nat" => info.flags.set(ServerFlags::NAT, p.parse()?), |
|
||||||
_ => { |
|
||||||
// skip unknown fields
|
|
||||||
let value = p.parse_bytes()?; |
|
||||||
if log_enabled!(Level::Debug) { |
|
||||||
let name = String::from_utf8_lossy(name); |
|
||||||
let value = String::from_utf8_lossy(value); |
|
||||||
debug!("Invalid ServerInfo field \"{}\" = \"{}\"", name, value); |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
Ok((challenge, info)) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
#[cfg(test)] |
|
||||||
mod tests { |
|
||||||
use super::*; |
|
||||||
use crate::parser::parse; |
|
||||||
|
|
||||||
#[test] |
|
||||||
fn parse_os() { |
|
||||||
assert_eq!(parse(b"\\l\\"), Ok(Os::Linux)); |
|
||||||
assert_eq!(parse(b"\\w\\"), Ok(Os::Windows)); |
|
||||||
assert_eq!(parse(b"\\m\\"), Ok(Os::Mac)); |
|
||||||
assert_eq!(parse::<Os>(b"\\u\\"), Ok(Os::Unknown)); |
|
||||||
} |
|
||||||
|
|
||||||
#[test] |
|
||||||
fn parse_server_type() { |
|
||||||
use ServerType as E; |
|
||||||
|
|
||||||
assert_eq!(parse(b"\\d\\"), Ok(E::Dedicated)); |
|
||||||
assert_eq!(parse(b"\\l\\"), Ok(E::Local)); |
|
||||||
assert_eq!(parse(b"\\p\\"), Ok(E::Proxy)); |
|
||||||
assert_eq!(parse::<E>(b"\\u\\"), Ok(E::Unknown)); |
|
||||||
} |
|
||||||
|
|
||||||
#[test] |
|
||||||
fn parse_region() { |
|
||||||
assert_eq!(parse(b"\\0\\"), Ok(Region::USEastCoast)); |
|
||||||
assert_eq!(parse(b"\\1\\"), Ok(Region::USWestCoast)); |
|
||||||
assert_eq!(parse(b"\\2\\"), Ok(Region::SouthAmerica)); |
|
||||||
assert_eq!(parse(b"\\3\\"), Ok(Region::Europe)); |
|
||||||
assert_eq!(parse(b"\\4\\"), Ok(Region::Asia)); |
|
||||||
assert_eq!(parse(b"\\5\\"), Ok(Region::Australia)); |
|
||||||
assert_eq!(parse(b"\\6\\"), Ok(Region::MiddleEast)); |
|
||||||
assert_eq!(parse(b"\\7\\"), Ok(Region::Africa)); |
|
||||||
assert_eq!(parse(b"\\-1\\"), Ok(Region::RestOfTheWorld)); |
|
||||||
assert_eq!(parse::<Region>(b"\\-2\\"), Err(Error::InvalidRegion)); |
|
||||||
assert_eq!( |
|
||||||
parse::<Region>(b"\\u\\"), |
|
||||||
Err(Error::Parser(ParserError::InvalidInteger)) |
|
||||||
); |
|
||||||
} |
|
||||||
|
|
||||||
#[test] |
|
||||||
fn parse_server_info() { |
|
||||||
let buf = b"\ |
|
||||||
\\protocol\\47\ |
|
||||||
\\challenge\\12345678\ |
|
||||||
\\players\\16\ |
|
||||||
\\max\\32\ |
|
||||||
\\bots\\1\ |
|
||||||
\\invalid_field\\field_value\ |
|
||||||
\\gamedir\\cstrike\ |
|
||||||
\\map\\de_dust\ |
|
||||||
\\type\\d\ |
|
||||||
\\password\\1\ |
|
||||||
\\os\\l\ |
|
||||||
\\secure\\1\ |
|
||||||
\\lan\\1\ |
|
||||||
\\version\\1.1.2.5\ |
|
||||||
\\region\\-1\ |
|
||||||
\\product\\cstrike\ |
|
||||||
\\nat\\1\ |
|
||||||
\ntail\ |
|
||||||
"; |
|
||||||
|
|
||||||
assert_eq!( |
|
||||||
ServerInfo::from_bytes(&buf[..]), |
|
||||||
Ok(( |
|
||||||
Some(12345678), |
|
||||||
ServerInfo::<&str> { |
|
||||||
protocol: 47, |
|
||||||
players: 16, |
|
||||||
max: 32, |
|
||||||
gamedir: "cstrike", |
|
||||||
map: "de_dust", |
|
||||||
server_type: ServerType::Dedicated, |
|
||||||
os: Os::Linux, |
|
||||||
version: "1.1.2.5", |
|
||||||
region: Region::RestOfTheWorld, |
|
||||||
product: "cstrike", |
|
||||||
flags: ServerFlags::all(), |
|
||||||
}, |
|
||||||
&b"tail"[..] |
|
||||||
)) |
|
||||||
); |
|
||||||
} |
|
||||||
} |
|
Loading…
Reference in new issue