Pumpkin/pumpkin-protocol/src/query.rs
2026-05-05 20:29:55 +02:00

415 lines
14 KiB
Rust

use std::{ffi::CString, fmt, io::Cursor};
use tokio::io::AsyncReadExt;
// Padding bytes are fixed by the vanilla server implementation.
// Some query checkers (incorrectly) depend on these exact bytes.
const PADDING_START: [u8; 11] = [
0x73, 0x70, 0x6C, 0x69, 0x74, 0x6E, 0x75, 0x6D, 0x00, 0x80, 0x00,
];
// 11 bytes: 10 padding bytes + 1 null terminator for the key-value section.
const PADDING_END: [u8; 11] = [
0x00, 0x01, 0x70, 0x6C, 0x61, 0x79, 0x65, 0x72, 0x5F, 0x00, 0x00,
];
/// The magic number that all Query protocol packets start with.
const MAGIC: u16 = 0xFEFD;
#[derive(Debug, PartialEq, Eq)]
pub enum DecodeError {
/// The byte stream ended before a complete packet could be read.
UnexpectedEof,
/// The first two bytes were not the expected magic value (0xFEFD).
BadMagic,
/// The packet type byte does not correspond to a known type.
UnknownPacketType(u8),
/// The packet was structurally valid but contained an unexpected payload length.
MalformedPayload,
}
impl fmt::Display for DecodeError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::UnexpectedEof => write!(f, "unexpected end of packet"),
Self::BadMagic => write!(f, "bad magic bytes (expected 0xFEFD)"),
Self::UnknownPacketType(t) => write!(f, "unknown packet type: {t:#04x}"),
Self::MalformedPayload => write!(f, "malformed packet payload"),
}
}
}
impl std::error::Error for DecodeError {}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PacketType {
Handshake = 9,
Status = 0,
}
impl TryFrom<u8> for PacketType {
type Error = DecodeError;
fn try_from(value: u8) -> Result<Self, Self::Error> {
match value {
9 => Ok(Self::Handshake),
0 => Ok(Self::Status),
other => Err(DecodeError::UnknownPacketType(other)),
}
}
}
/// A partially-decoded packet: magic verified, type extracted, payload buffered.
#[derive(Debug, PartialEq, Eq)]
pub struct RawQueryPacket {
pub packet_type: PacketType,
reader: Cursor<Vec<u8>>,
}
impl RawQueryPacket {
pub async fn decode(bytes: Vec<u8>) -> Result<Self, DecodeError> {
let mut reader = Cursor::new(bytes);
let magic = reader
.read_u16()
.await
.map_err(|_| DecodeError::UnexpectedEof)?;
if magic != MAGIC {
return Err(DecodeError::BadMagic);
}
let type_byte = reader
.read_u8()
.await
.map_err(|_| DecodeError::UnexpectedEof)?;
let packet_type = PacketType::try_from(type_byte)?;
Ok(Self {
packet_type,
reader,
})
}
}
#[derive(PartialEq, Eq, Debug)]
pub struct SHandshake {
pub session_id: i32,
}
impl SHandshake {
pub async fn decode(packet: &mut RawQueryPacket) -> Result<Self, DecodeError> {
let session_id = packet
.reader
.read_i32()
.await
.map_err(|_| DecodeError::UnexpectedEof)?;
Ok(Self { session_id })
}
}
#[derive(PartialEq, Eq, Debug)]
pub struct SStatusRequest {
pub session_id: i32,
pub challenge_token: i32,
/// `true` for a full-status request (payload padded to 8 extra bytes),
/// `false` for a basic-status request (no padding).
pub is_full_request: bool,
}
impl SStatusRequest {
pub async fn decode(packet: &mut RawQueryPacket) -> Result<Self, DecodeError> {
let session_id = packet
.reader
.read_i32()
.await
.map_err(|_| DecodeError::UnexpectedEof)?;
let challenge_token = packet
.reader
.read_i32()
.await
.map_err(|_| DecodeError::UnexpectedEof)?;
let position = packet.reader.position();
let total_len = packet.reader.get_ref().len() as u64;
let remaining = total_len.saturating_sub(position);
let is_full_request = match remaining {
0 => false,
4 => {
packet.reader.set_position(position + 4);
true
}
_ => return Err(DecodeError::MalformedPayload),
};
Ok(Self {
session_id,
challenge_token,
is_full_request,
})
}
}
#[derive(Debug)]
pub struct CHandshake {
pub session_id: i32,
/// Encoded as a decimal string in the wire format.
pub challenge_token: i32,
}
impl CHandshake {
#[must_use]
pub fn encode(&self) -> Option<Vec<u8>> {
let token = CString::new(self.challenge_token.to_string()).ok()?;
let mut buf = Vec::with_capacity(6 + token.as_bytes_with_nul().len());
buf.push(PacketType::Handshake as u8);
buf.extend_from_slice(&self.session_id.to_be_bytes());
buf.extend_from_slice(token.as_bytes_with_nul());
Some(buf)
}
}
#[derive(Debug)]
pub struct CBasicStatus {
pub session_id: i32,
pub motd: CString,
pub map: CString,
pub num_players: usize,
pub max_players: usize,
/// Little-endian on the wire (Notchian quirk).
pub host_port: u16,
pub host_ip: CString,
}
impl CBasicStatus {
#[must_use]
pub fn encode(&self) -> Option<Vec<u8>> {
let game_type = CString::new("SMP").ok()?;
let num_players = CString::new(self.num_players.to_string()).ok()?;
let max_players = CString::new(self.max_players.to_string()).ok()?;
let mut buf = Vec::new();
buf.push(PacketType::Status as u8);
buf.extend_from_slice(&self.session_id.to_be_bytes());
buf.extend_from_slice(self.motd.as_bytes_with_nul());
buf.extend_from_slice(game_type.as_bytes_with_nul());
buf.extend_from_slice(self.map.as_bytes_with_nul());
buf.extend_from_slice(num_players.as_bytes_with_nul());
buf.extend_from_slice(max_players.as_bytes_with_nul());
// The port is written little-endian — this is a known Notchian quirk.
buf.extend_from_slice(&self.host_port.to_le_bytes());
buf.extend_from_slice(self.host_ip.as_bytes_with_nul());
Some(buf)
}
}
#[derive(Debug)]
pub struct CFullStatus {
pub session_id: i32,
pub hostname: CString,
pub version: CString,
pub plugins: CString,
pub map: CString,
pub num_players: usize,
pub max_players: usize,
/// Little-endian on the wire (Notchian quirk).
pub host_port: u16,
pub host_ip: CString,
pub players: Vec<CString>,
}
impl CFullStatus {
#[must_use]
pub fn encode(&self) -> Option<Vec<u8>> {
let mut buf = Vec::new();
buf.push(PacketType::Status as u8);
buf.extend_from_slice(&self.session_id.to_be_bytes());
buf.extend_from_slice(&PADDING_START);
let kv_pairs: &[(&str, &CString)] = &[
("hostname", &self.hostname),
("gametype", &CString::new("SMP").ok()?),
("game_id", &CString::new("MINECRAFT").ok()?),
("version", &self.version),
("plugins", &self.plugins),
("map", &self.map),
(
"numplayers",
&CString::new(self.num_players.to_string()).ok()?,
),
(
"maxplayers",
&CString::new(self.max_players.to_string()).ok()?,
),
("hostport", &CString::new(self.host_port.to_string()).ok()?),
("hostip", &self.host_ip),
];
for (key, value) in kv_pairs {
buf.extend_from_slice(CString::new(*key).ok()?.as_bytes_with_nul());
buf.extend_from_slice(value.as_bytes_with_nul());
}
// End padding + null terminator for the key-value section.
buf.extend_from_slice(&PADDING_END);
// Player list, each null-terminated, followed by a final null.
for player in &self.players {
buf.extend_from_slice(player.as_bytes_with_nul());
}
buf.push(0x00);
Some(buf)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn handshake_request() -> Result<(), Box<dyn std::error::Error>> {
let bytes = vec![0xFE, 0xFD, 0x09, 0x00, 0x00, 0x00, 0x01];
let mut raw = RawQueryPacket::decode(bytes).await?;
assert_eq!(raw.packet_type, PacketType::Handshake);
let pkt = SHandshake::decode(&mut raw).await?;
assert_eq!(pkt, SHandshake { session_id: 1 });
Ok(())
}
#[tokio::test]
async fn handshake_response() -> Result<(), Box<dyn std::error::Error>> {
let expected = vec![
0x09, 0x00, 0x00, 0x00, 0x01, 0x39, 0x35, 0x31, 0x33, 0x33, 0x30, 0x37, 0x00,
];
let pkt = CHandshake {
session_id: 1,
challenge_token: 9513307,
};
assert_eq!(pkt.encode().ok_or("Encoding failed")?, expected);
Ok(())
}
#[tokio::test]
async fn basic_stat_request() -> Result<(), Box<dyn std::error::Error>> {
let bytes = vec![
0xFE, 0xFD, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x91, 0x29, 0x5B,
];
let mut raw = RawQueryPacket::decode(bytes).await?;
let pkt = SStatusRequest::decode(&mut raw).await?;
assert_eq!(
pkt,
SStatusRequest {
session_id: 1,
challenge_token: 9513307,
is_full_request: false
}
);
Ok(())
}
#[tokio::test]
async fn basic_stat_response() -> Result<(), Box<dyn std::error::Error>> {
let expected = vec![
0x00, 0x00, 0x00, 0x00, 0x01, 0x41, 0x20, 0x4D, 0x69, 0x6E, 0x65, 0x63, 0x72, 0x61,
0x66, 0x74, 0x20, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x00, 0x53, 0x4D, 0x50, 0x00,
0x77, 0x6F, 0x72, 0x6C, 0x64, 0x00, 0x32, 0x00, 0x32, 0x30, 0x00, 0xDD, 0x63, 0x31,
0x32, 0x37, 0x2E, 0x30, 0x2E, 0x30, 0x2E, 0x31, 0x00,
];
let pkt = CBasicStatus {
session_id: 1,
motd: CString::new("A Minecraft Server")?,
map: CString::new("world")?,
num_players: 2,
max_players: 20,
host_port: 25565,
host_ip: CString::new("127.0.0.1")?,
};
assert_eq!(pkt.encode().ok_or("Encoding failed")?, expected);
Ok(())
}
#[tokio::test]
async fn full_stat_request() -> Result<(), Box<dyn std::error::Error>> {
let bytes = vec![
0xFE, 0xFD, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x91, 0x29, 0x5B, 0x00, 0x00, 0x00,
0x00,
];
let mut raw = RawQueryPacket::decode(bytes).await?;
let pkt = SStatusRequest::decode(&mut raw).await?;
assert_eq!(
pkt,
SStatusRequest {
session_id: 1,
challenge_token: 9513307,
is_full_request: true
}
);
Ok(())
}
#[tokio::test]
async fn full_stat_response() -> Result<(), Box<dyn std::error::Error>> {
let expected = vec![
0x00, 0x00, 0x00, 0x00, 0x01, 0x73, 0x70, 0x6C, 0x69, 0x74, 0x6E, 0x75, 0x6D, 0x00,
0x80, 0x00, 0x68, 0x6F, 0x73, 0x74, 0x6E, 0x61, 0x6D, 0x65, 0x00, 0x41, 0x20, 0x4D,
0x69, 0x6E, 0x65, 0x63, 0x72, 0x61, 0x66, 0x74, 0x20, 0x53, 0x65, 0x72, 0x76, 0x65,
0x72, 0x00, 0x67, 0x61, 0x6D, 0x65, 0x74, 0x79, 0x70, 0x65, 0x00, 0x53, 0x4D, 0x50,
0x00, 0x67, 0x61, 0x6D, 0x65, 0x5F, 0x69, 0x64, 0x00, 0x4D, 0x49, 0x4E, 0x45, 0x43,
0x52, 0x41, 0x46, 0x54, 0x00, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6F, 0x6E, 0x00, 0x42,
0x65, 0x74, 0x61, 0x20, 0x31, 0x2E, 0x39, 0x20, 0x50, 0x72, 0x65, 0x72, 0x65, 0x6C,
0x65, 0x61, 0x73, 0x65, 0x20, 0x34, 0x00, 0x70, 0x6C, 0x75, 0x67, 0x69, 0x6E, 0x73,
0x00, 0x00, 0x6D, 0x61, 0x70, 0x00, 0x77, 0x6F, 0x72, 0x6C, 0x64, 0x00, 0x6E, 0x75,
0x6D, 0x70, 0x6C, 0x61, 0x79, 0x65, 0x72, 0x73, 0x00, 0x32, 0x00, 0x6D, 0x61, 0x78,
0x70, 0x6C, 0x61, 0x79, 0x65, 0x72, 0x73, 0x00, 0x32, 0x30, 0x00, 0x68, 0x6F, 0x73,
0x74, 0x70, 0x6F, 0x72, 0x74, 0x00, 0x32, 0x35, 0x35, 0x36, 0x35, 0x00, 0x68, 0x6F,
0x73, 0x74, 0x69, 0x70, 0x00, 0x31, 0x32, 0x37, 0x2E, 0x30, 0x2E, 0x30, 0x2E, 0x31,
0x00, 0x00, 0x01, 0x70, 0x6C, 0x61, 0x79, 0x65, 0x72, 0x5F, 0x00, 0x00, 0x62, 0x61,
0x72, 0x6E, 0x65, 0x79, 0x67, 0x61, 0x6C, 0x65, 0x00, 0x56, 0x69, 0x76, 0x61, 0x6C,
0x61, 0x68, 0x65, 0x6C, 0x76, 0x69, 0x67, 0x00, 0x00,
];
let pkt = CFullStatus {
session_id: 1,
hostname: CString::new("A Minecraft Server")?,
version: CString::new("Beta 1.9 Prerelease 4")?,
plugins: CString::new("")?,
map: CString::new("world")?,
num_players: 2,
max_players: 20,
host_port: 25565,
host_ip: CString::new("127.0.0.1")?,
players: vec![CString::new("barneygale")?, CString::new("Vivalahelvig")?],
};
assert_eq!(pkt.encode().ok_or("Encoding failed")?, expected);
Ok(())
}
#[tokio::test]
async fn bad_magic_rejected() {
let bytes = vec![0xDE, 0xAD, 0x09, 0x00, 0x00, 0x00, 0x01];
assert_eq!(
RawQueryPacket::decode(bytes).await,
Err(DecodeError::BadMagic)
);
}
#[tokio::test]
async fn unknown_packet_type_rejected() {
let bytes = vec![0xFE, 0xFD, 0xFF];
assert_eq!(
RawQueryPacket::decode(bytes).await,
Err(DecodeError::UnknownPacketType(0xFF))
);
}
#[tokio::test]
async fn truncated_packet_rejected() {
let bytes = vec![0xFE, 0xFD];
assert_eq!(
RawQueryPacket::decode(bytes).await,
Err(DecodeError::UnexpectedEof)
);
}
}