Very late initial commit
This commit is contained in:
507
src/lib.rs
Normal file
507
src/lib.rs
Normal file
@@ -0,0 +1,507 @@
|
||||
#![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());
|
||||
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))
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user