mcquery-rs/src/lib.rs

507 lines
16 KiB
Rust

#![deny(missing_docs)]
//! This library provides basically two functions for querying Minecraft
//! servers. For increased stability, an extra function for handshakes
//! is not provided.
//!
//! ## Usage
//! To perform a query, call [`Query::get_basic`] or [`Query::get_full`].
//! The returned response will either be a [`BasicQueryResponse`] or a
//! [`FullQueryResponse`] respectively.
//!
//! ## Development resources
//! Thank you to the [Tokio project] and the [Minecraft Developer Wiki] for
//! providing a great asynchronous runtime and useful documentation!
//!
//! Without them, this crate wouldn't be possible.
//!
//! [Tokio project]: https://tokio.rs/
//! [Minecraft Developer Wiki]: https://wiki.vg/
use std::{
collections::HashMap,
io,
time::Duration
};
use tokio::{
net::{ ToSocketAddrs, UdpSocket },
time::timeout as tokioTimeout,
};
/// Bitmask to validate a session ID
///
/// Usually you don't need this.
///
/// # Example
/// ```
/// use mcquery::SESSION_ID_MASK;
///
/// # #[allow(unused_variables)]
/// # fn main() {
/// let session_id = 0x12345678_u32;
/// let valid_session_id = session_id & SESSION_ID_MASK;
/// # }
/// ```
pub const SESSION_ID_MASK: u32 = 0x0F0F0F0F;
const REQUEST_HEADER: [u8; 2] = [0xFE, 0xFD];
static mut SESSION_ID_COUNTER: u16 = 0;
fn gen_session_id() -> u32 {
unsafe {
SESSION_ID_COUNTER = SESSION_ID_COUNTER.wrapping_add(1);
}
let mut session_id_bytes = [0; 4];
for (i, b) in unsafe { SESSION_ID_COUNTER }.to_be_bytes().iter().enumerate() {
session_id_bytes[i * 2] = b >> 4;
session_id_bytes[i * 2 + 1] = b & 0x0F;
}
u32::from_be_bytes(session_id_bytes)
}
/// Encountered when the buffer is full.
///
/// # Desciption
/// The response may be incomplete, if the buffer has been filled up,
/// because excess bytes that don't fit into it are discarded.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub struct MaybeIncompleteDataError(usize);
impl std::error::Error for MaybeIncompleteDataError {}
impl core::fmt::Display for MaybeIncompleteDataError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(f, "Buffer filled up")
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
enum PacketType {
Handshake = 0x09,
Query = 0x00,
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
struct HandshakeRequest {
session_id: u32,
}
impl HandshakeRequest {
const PACKET_TYPE: PacketType = PacketType::Handshake;
fn bytes(&self) -> Vec<u8> {
let mut vec = Vec::new();
vec.extend_from_slice(&REQUEST_HEADER);
vec.push(Self::PACKET_TYPE as u8);
vec.extend_from_slice(&self.session_id.to_be_bytes());
vec
}
async fn send(&self, socket: &UdpSocket, timeout: Duration) -> io::Result<HandshakeResponse> {
// Sending
tokioTimeout(timeout, socket.send(self.bytes().as_slice())).await??;
// Receiving
let mut response_buffer = [0; 16];
let bytes_read = tokioTimeout(timeout, socket.recv(&mut response_buffer)).await??;
// Parsing
let mut response_buffer = response_buffer[..bytes_read].to_owned();
let res_packet_type = response_buffer.remove(0);
let mut res_session_id = [0; 4];
for (i, b) in response_buffer.drain(..4).enumerate() {
res_session_id[i] = b;
}
let res_session_id = u32::from_be_bytes(res_session_id);
let res_challenge_token =
String::from_utf8(
response_buffer
.into_iter()
.take_while(|x| x != &0x00)
.collect()
).map_err(|_| io::ErrorKind::InvalidData)?
.parse()
.map_err(|_| io::ErrorKind::InvalidData)?;
if res_packet_type != Self::PACKET_TYPE as u8 || res_session_id != self.session_id {
Err(io::ErrorKind::InvalidData.into())
} else {
Ok(HandshakeResponse {
session_id: res_session_id,
challenge_token: res_challenge_token,
})
}
}
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
struct HandshakeResponse {
session_id: u32,
challenge_token: i32,
}
impl From<HandshakeResponse> for BasicQueryRequest {
fn from(res: HandshakeResponse) -> Self {
BasicQueryRequest {
session_id: res.session_id,
challenge_token: res.challenge_token,
}
}
}
impl From<HandshakeResponse> for FullQueryRequest {
fn from(res: HandshakeResponse) -> Self {
FullQueryRequest {
session_id: res.session_id,
challenge_token: res.challenge_token,
}
}
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
struct BasicQueryRequest {
session_id: u32,
challenge_token: i32,
}
impl BasicQueryRequest {
const PACKET_TYPE: PacketType = PacketType::Query;
fn bytes(&self) -> Vec<u8> {
let mut vec = Vec::new();
vec.extend_from_slice(&REQUEST_HEADER);
vec.push(Self::PACKET_TYPE as u8);
vec.extend_from_slice(&self.session_id.to_be_bytes());
vec.extend_from_slice(&self.challenge_token.to_be_bytes());
vec
}
async fn send(&self, socket: &UdpSocket, timeout: Duration, buffer: &mut [u8]) -> io::Result<BasicQueryResponse> {
// Sending
tokioTimeout(timeout, socket.send(self.bytes().as_slice())).await??;
// Receiving
let bytes_read = tokioTimeout(timeout, socket.recv(buffer)).await??;
if bytes_read == buffer.len() {
return Err(io::Error::new(io::ErrorKind::Other, MaybeIncompleteDataError(bytes_read)));
}
// Parsing
let mut response_buffer: String = buffer[0..bytes_read].iter().map(|b| *b as char).collect();
// Packet type
let res_packet_type = response_buffer.remove(0) as u8;
// Session ID
let mut res_session_id = [0; 4];
for (i, b) in response_buffer.drain(..4).enumerate() {
res_session_id[i] = b as u8;
}
let res_session_id = u32::from_be_bytes(res_session_id);
let (
res_motd,
res_gametype,
res_map,
res_num_players,
res_max_players,
mut res_address
) = {
let mut i = 0;
let vec: Vec<_> = response_buffer
.split_terminator(|c| {
if i < 5 {
if c == '\0' {
i += 1;
}
c == '\0'
} else {
i += 1;
i > 7 && c == '\0'
}
})
.collect();
(
vec[0].clone(),
vec[1].clone(),
vec[2].clone(),
vec[3].clone(),
vec[4].clone(),
vec[5].chars().map(|c| c as u8).collect::<Vec<_>>(),
)
};
// Number of players
let res_num_players = res_num_players.parse().map_err(|_| io::ErrorKind::InvalidData)?;
// Max number of players
let res_max_players = res_max_players.parse().map_err(|_| io::ErrorKind::InvalidData)?;
// Host port
let mut res_port = [0; 2];
for (i, b) in res_address.drain(..2).enumerate() {
res_port[i] = b;
}
let res_port = u16::from_le_bytes(res_port);
// Host IP
let res_ip = String::from_utf8(res_address).map_err(|_| io::ErrorKind::InvalidData)?;
if res_packet_type != Self::PACKET_TYPE as u8 || res_session_id != self.session_id {
Err(io::ErrorKind::InvalidData.into())
} else {
Ok(BasicQueryResponse {
session_id: res_session_id,
motd: res_motd.to_owned(),
gametype: res_gametype.to_owned(),
map: res_map.to_owned(),
num_players: res_num_players,
max_players: res_max_players,
port: res_port,
ip: res_ip,
})
}
}
}
/// Response to a basic query
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct BasicQueryResponse {
session_id: u32,
/// The Motto of the Day
pub motd: String,
/// Should always be `"SMP"`
pub gametype: String,
/// The default map
pub map: String,
/// Number of players currently online
pub num_players: u32,
/// Maximum player capacity
pub max_players: u32,
/// The host port. Default is `25565`
pub port: u16,
/// The host ip. This is the localhost by default
pub ip: String,
}
struct FullQueryRequest {
session_id: u32,
challenge_token: i32,
}
impl FullQueryRequest {
const PACKET_TYPE: PacketType = PacketType::Query;
fn bytes(&self) -> Vec<u8> {
let mut vec = Vec::new();
vec.extend_from_slice(&REQUEST_HEADER);
vec.push(Self::PACKET_TYPE as u8);
vec.extend_from_slice(&self.session_id.to_be_bytes());
vec.extend_from_slice(&self.challenge_token.to_be_bytes());
vec.extend_from_slice(&[0; 4]);
vec
}
async fn send(&self, socket: &UdpSocket, timeout: Duration, buffer: &mut [u8]) -> io::Result<FullQueryResponse> {
// Sending
tokioTimeout(timeout, socket.send(self.bytes().as_slice())).await??;
// Receiving
let bytes_read = tokioTimeout(timeout, socket.recv(buffer)).await??;
if bytes_read == buffer.len() {
return Err(io::Error::new(io::ErrorKind::Other, MaybeIncompleteDataError(bytes_read)));
}
// Parsing
let mut response_buffer = buffer[0..bytes_read].to_vec();
// Packet type
let res_packet_type = response_buffer.remove(0);
// Session ID
let mut res_session_id = [0; 4];
for (i, b) in response_buffer.drain(..4).enumerate() {
res_session_id[i] = b
}
let res_session_id = u32::from_be_bytes(res_session_id);
// Key value section
let mut res_kv = HashMap::<String, String>::new();
loop {
let key: Vec<u8> = response_buffer.iter()
.take_while(|b| **b != 0x00)
.map(|&b| b)
.collect();
response_buffer.drain(0..key.len() + 1);
if key.len() == 0 {
break;
}
let value: Vec<u8> = response_buffer.iter()
.take_while(|b| **b != 0x00)
.map(|&b| b)
.collect();
response_buffer.drain(0..value.len() + 1);
res_kv.insert(
key.iter()
.map(|b| *b as char)
.collect(),
value.iter()
.map(|b| *b as char)
.collect()
);
}
res_kv.remove("splitnum").ok_or(io::ErrorKind::InvalidData)?;
// Plugins and server software
let (
res_software,
res_plugins
) = if let Some(mut plugins) = res_kv.get("plugins")
.map(|s| s.clone())
{
res_kv.remove("plugins");
// let mut plugin_string = plugin_string;
let software: String = plugins.chars().take_while(|c| *c != ':').collect();
plugins.drain(0..software.len() + 1);
let plugins = if plugins.len() > 0 {
Some(plugins.split(';').map(|s| s.trim().to_owned()).collect())
} else {
None
};
(Some(software), plugins)
} else {
(None, None)
};
// Players
{
let player_header = "\x01player_\0\0";
if !response_buffer.starts_with(player_header.as_bytes()) {
return Err(io::ErrorKind::InvalidData.into());
} else {
response_buffer.drain(0..player_header.len());
}
}
let mut res_players = Vec::new();
loop {
let player: Vec<u8> = response_buffer.iter()
.take_while(|b| **b != 0x00)
.map(|&b| b)
.collect();
response_buffer.drain(0..player.len() + 1);
if player.len() == 0 {
break;
}
res_players.push(
player.iter()
.map(|b| *b as char)
.collect()
);
}
if res_packet_type != Self::PACKET_TYPE as u8 || res_session_id != self.session_id {
Err(io::ErrorKind::InvalidData.into())
} else {
Ok(FullQueryResponse {
session_id: res_session_id,
kv: res_kv,
players: res_players,
software: res_software,
plugins: res_plugins,
})
}
}
}
/// Response to a full query
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FullQueryResponse {
session_id: u32,
/// Additional server information
pub kv: HashMap<String, String>,
/// Names of players currently online
pub players: Vec<String>,
/// The software string. Empty if vanilla server or not supported
pub software: Option<String>,
/// Plugins used on the server. Empty if vanilla server or not supported
pub plugins: Option<Vec<String>>
}
/// The main data structure in this crate
///
/// # Description
/// Basic queries are enabled by default. They can be diabled in the server's
/// `server.properties` with `enable-status=true`. This is what is used in the
/// server list inside Minecraft.
///
/// Full queries are diabled by default. They can be enabled in the server's
/// `server.properties` with `enable-query=true`.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Query {
/// Response to a basic query. Can be obtained with [`Query::get_basic()`]
///
/// See [`BasicQueryResponse`] for more
Basic(BasicQueryResponse),
/// Response to a full query. Can be obtained with [`Query::get_full()`]
///
/// See [`FullQueryResponse`] for more
Full(FullQueryResponse),
}
impl Query {
/// Connects with the Minecraft server at `address` and performs a basic query.
/// `timeout` is used as a response timeout. The specified buffer needs to be big
/// enough to hold all response bytes, else this function will return a
/// [`MaybeIncompleteDataError`] wrapped in an [`std::io::Error`]. The [`Ok`] value
/// will be [`Query::Basic`].
pub async fn get_basic<A: ToSocketAddrs>(address: A, timeout: Duration, buffer: &mut [u8]) -> io::Result<Self> {
let socket = UdpSocket::bind("0.0.0.0:0").await?;
socket.connect(address).await?;
let handshake_req = HandshakeRequest {
session_id: gen_session_id(),
};
let handshake_res = handshake_req.send(&socket, timeout).await?;
let query_req: BasicQueryRequest = handshake_res.into();
buffer.iter_mut().for_each(|b| *b = 0);
let query_res = query_req.send(&socket, timeout, buffer).await?;
Ok(Query::Basic(query_res))
}
/// Connects with the Minecraft server at `address` and performs a full query.
/// `timeout` is used as a response timeout. The specified buffer needs to be big
/// enough to hold all response bytes, else this function will return a
/// [`MaybeIncompleteDataError`] wrapped in an [`std::io::Error`]. The [`Ok`] vlaue
/// will be [`Query::Full`].
pub async fn get_full<A: ToSocketAddrs>(address: A, timeout: Duration, buffer: &mut [u8]) -> io::Result<Self> {
let socket = UdpSocket::bind("0.0.0.0:0").await?;
socket.connect(address).await?;
let handshake_req = HandshakeRequest {
session_id: gen_session_id(),
};
let handshake_res = handshake_req.send(&socket, timeout).await?;
let query_req: FullQueryRequest = handshake_res.into();
buffer.iter_mut().for_each(|b| *b = 0);
let query_res = query_req.send(&socket, timeout, buffer).await?;
Ok(Query::Full(query_res))
}
}