Implement Query protocol (#218)

* Add query options to config
Added packet data types
Correctly parse a handshake packet
Add tokio to pumpkin protocol for `AsyncReadExt` convenience functions

* Add repr(u8) so that PacketType is a byte

* Respond to handshake correctly

* Allow for proper decoding of status packets and properly handle errors in decoding

* Implement challange tokens and verify with token and address

* Encode Basic status packet
Switch to CString since its better to and null errors higher up the code

* Encode Full status packet

* Add forgotten fields to full status packet

* Correctly respond to query clients when requesting full status

* Return actual address and port of server
Respect config options
Remove uncessary debug derives

* Remove packet type as it is redundant/unnecssary

* Implement basic status request
Refactor code for better error handling

* Update README

* Show players correctly in full status packet

* Store all packets in structs instead of enums
Break up en/decoding for each packet
Reduce client tracking
Get rid of QueryClients struct
Stop storing of magic value and check during decoding
Added tests for all decoding and encoding
Fix all sugestions from @StripedMonkey
This commit is contained in:
we sell insurance
2024-11-05 13:07:31 -06:00
committed by GitHub
parent 1f8385e890
commit 93c6da993f
8 changed files with 553 additions and 4 deletions

View File

@@ -63,7 +63,7 @@ and customizable experience. It prioritizes performance and player enjoyment whi
- [x] Player Combat - [x] Player Combat
- Server - Server
- [ ] Plugins - [ ] Plugins
- [ ] Query - [x] Query
- [x] RCON - [x] RCON
- [x] Inventories - [x] Inventories
- [x] Particles - [x] Particles

View File

@@ -1,6 +1,7 @@
use log::warn; use log::warn;
use logging::LoggingConfig; use logging::LoggingConfig;
use pumpkin_core::{Difficulty, GameMode}; use pumpkin_core::{Difficulty, GameMode};
use query::QueryConfig;
use serde::{de::DeserializeOwned, Deserialize, Serialize}; use serde::{de::DeserializeOwned, Deserialize, Serialize};
// TODO: when https://github.com/rust-lang/rfcs/pull/3681 gets merged, replace serde-inline-default with native syntax // TODO: when https://github.com/rust-lang/rfcs/pull/3681 gets merged, replace serde-inline-default with native syntax
@@ -16,6 +17,7 @@ use std::{
pub mod auth; pub mod auth;
pub mod logging; pub mod logging;
pub mod proxy; pub mod proxy;
pub mod query;
pub mod resource_pack; pub mod resource_pack;
pub use auth::AuthenticationConfig; pub use auth::AuthenticationConfig;
@@ -53,6 +55,7 @@ pub struct AdvancedConfiguration {
pub rcon: RCONConfig, pub rcon: RCONConfig,
pub pvp: PVPConfig, pub pvp: PVPConfig,
pub logging: LoggingConfig, pub logging: LoggingConfig,
pub query: QueryConfig,
} }
#[serde_inline_default] #[serde_inline_default]

View File

@@ -0,0 +1,12 @@
use serde::{Deserialize, Serialize};
use serde_inline_default::serde_inline_default;
#[serde_inline_default]
#[derive(Deserialize, Serialize, Default)]
pub struct QueryConfig {
#[serde_inline_default(false)]
pub enabled: bool,
// Optional so if not specified the port server is running on will be used
#[serde_inline_default(None)]
pub port: Option<u16>,
}

View File

@@ -14,7 +14,7 @@ serde.workspace = true
thiserror.workspace = true thiserror.workspace = true
itertools.workspace = true itertools.workspace = true
log.workspace = true log.workspace = true
tokio.workspace = true
num-traits.workspace = true num-traits.workspace = true
num-derive.workspace = true num-derive.workspace = true

View File

@@ -6,6 +6,7 @@ pub mod bytebuf;
pub mod client; pub mod client;
pub mod packet_decoder; pub mod packet_decoder;
pub mod packet_encoder; pub mod packet_encoder;
pub mod query;
pub mod server; pub mod server;
pub mod slot; pub mod slot;

View File

@@ -0,0 +1,350 @@
use std::{ffi::CString, io::Cursor};
use num_derive::FromPrimitive;
use num_traits::FromPrimitive;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
#[derive(FromPrimitive)]
#[repr(u8)]
pub enum PacketType {
// There could be other types but they are not documented
// Besides these types are enough to get server status
Handshake = 9,
Status = 0,
}
pub struct RawQueryPacket {
pub packet_type: PacketType,
reader: Cursor<Vec<u8>>,
}
impl RawQueryPacket {
pub async fn decode(bytes: Vec<u8>) -> Result<Self, ()> {
let mut reader = Cursor::new(bytes);
match reader.read_u16().await.map_err(|_| ())? {
// Magic should always equal 65277
// Since it denotes the protocl being used
// Should not attempt to decode packets with other magic values
65277 => Ok(Self {
packet_type: PacketType::from_u8(reader.read_u8().await.map_err(|_| ())?)
.ok_or(())?,
reader,
}),
_ => Err(()),
}
}
}
#[derive(PartialEq, Debug)]
pub struct SHandshake {
pub session_id: i32,
}
impl SHandshake {
pub async fn decode(packet: &mut RawQueryPacket) -> Result<Self, ()> {
Ok(Self {
session_id: packet.reader.read_i32().await.map_err(|_| ())?,
})
}
}
#[derive(PartialEq, Debug)]
pub struct SStatusRequest {
pub session_id: i32,
pub challange_token: i32,
// Full status request and basic status request are pretty much similar
// So might as just use the same struct
pub is_full_request: bool,
}
impl SStatusRequest {
pub async fn decode(packet: &mut RawQueryPacket) -> Result<Self, ()> {
Ok(Self {
session_id: packet.reader.read_i32().await.map_err(|_| ())?,
challange_token: packet.reader.read_i32().await.map_err(|_| ())?,
is_full_request: {
let mut buf = [0; 4];
// If payload is padded to 8 bytes, the client is requesting full status response
// In other terms, check if there are 4 extra bytes at the end
// The extra bytes should be meaningless
// Otherwise the client is requesting basic status response
match packet.reader.read(&mut buf).await {
Ok(0) => false,
Ok(4) => true,
_ => {
// Just ingnore malformed packets or errors
return Err(());
}
}
},
})
}
}
pub struct CHandshake {
pub session_id: i32,
// For simplicity use a number type
// Should be encoded as string here
// Will be converted in encoding
pub challange_token: i32,
}
impl CHandshake {
pub async fn encode(&self) -> Vec<u8> {
let mut buf = Vec::new();
// Packet Type
buf.write_u8(9).await.unwrap();
// Session ID
buf.write_i32(self.session_id).await.unwrap();
// Challange token
// Use CString to add null terminator and ensure no null bytes in the middle of data
// Unwrap here since there should be no errors with nulls in the middle of data
let token = CString::new(self.challange_token.to_string()).unwrap();
buf.extend_from_slice(token.as_bytes_with_nul());
buf
}
}
pub struct CBasicStatus {
pub session_id: i32,
// Use CString as protocol requires nul terminated strings
pub motd: CString,
// Game type is hardcoded
pub map: CString,
pub num_players: usize,
pub max_players: usize,
pub host_port: u16,
pub host_ip: CString,
}
impl CBasicStatus {
pub async fn encode(&self) -> Vec<u8> {
let mut buf = Vec::new();
// Packet Type
buf.write_u8(0).await.unwrap();
// Session ID
buf.write_i32(self.session_id).await.unwrap();
// MOTD
buf.extend_from_slice(self.motd.as_bytes_with_nul());
// Game Type
let game_type = CString::new("SMP").unwrap();
buf.extend_from_slice(game_type.as_bytes_with_nul());
// Map
buf.extend_from_slice(self.map.as_bytes_with_nul());
// Num players
let num_players = CString::new(self.num_players.to_string()).unwrap();
buf.extend_from_slice(num_players.as_bytes_with_nul());
// Max players
let max_players = CString::new(self.max_players.to_string()).unwrap();
buf.extend_from_slice(max_players.as_bytes_with_nul());
// Port
// No idea why the port needs to be in little endian
buf.write_u16_le(self.host_port).await.unwrap();
// IP
buf.extend_from_slice(self.host_ip.as_bytes_with_nul());
buf
}
}
pub struct CFullStatus {
pub session_id: i32,
pub hostname: CString,
// Game type and game id are hardcoded into protocol
// They are not here as they cannot be changed
pub version: CString,
pub plugins: CString,
pub map: CString,
pub num_players: usize,
pub max_players: usize,
pub host_port: u16,
pub host_ip: CString,
pub players: Vec<CString>,
}
impl CFullStatus {
pub async fn encode(&self) -> Vec<u8> {
let mut buf = Vec::new();
// Packet type
buf.write_u8(0).await.unwrap();
// Session ID
buf.write_i32(self.session_id).await.unwrap();
// Padding (11 bytes, meaningless)
// This is the padding used by vanilla
// Although meaningless, in testing some query checkers depend on these bytes?
const PADDING_START: [u8; 11] = [
0x73, 0x70, 0x6C, 0x69, 0x74, 0x6E, 0x75, 0x6D, 0x00, 0x80, 0x00,
];
buf.extend_from_slice(PADDING_START.as_slice());
// Key-value pairs
// Keys will not error when encoding as CString
for (key, value) in [
("hostname", &self.hostname),
("gametype", &CString::new("SMP").unwrap()),
("game_id", &CString::new("MINECRAFT").unwrap()),
("version", &self.version),
("plugins", &self.plugins),
("map", &self.map),
(
"numplayers",
&CString::new(self.num_players.to_string()).unwrap(),
),
(
"maxplayers",
&CString::new(self.max_players.to_string()).unwrap(),
),
(
"hostport",
&CString::new(self.host_port.to_string()).unwrap(),
),
("hostip", &self.host_ip),
] {
buf.extend_from_slice(CString::new(key).unwrap().as_bytes_with_nul());
buf.extend_from_slice(value.as_bytes_with_nul());
}
// Padding (10 bytes, meaningless), with one extra 0x00 for the extra required null terminator after the Key Value section
const PADDING_END: [u8; 11] = [
0x00, 0x01, 0x70, 0x6C, 0x61, 0x79, 0x65, 0x72, 0x5F, 0x00, 0x00,
];
buf.extend_from_slice(PADDING_END.as_slice());
// Players
for player in &self.players {
buf.extend_from_slice(player.as_bytes_with_nul());
}
// Required extra null terminator
buf.write_u8(0).await.unwrap();
buf
}
}
// All test bytes/packets are from protocol documentation
#[tokio::test]
async fn test_handshake_request() {
let bytes = vec![0xFE, 0xFD, 0x09, 0x00, 0x00, 0x00, 0x01];
let mut raw_packet = RawQueryPacket::decode(bytes).await.unwrap();
let packet = SHandshake::decode(&mut raw_packet).await.unwrap();
// What the decoded packet should look like
let actual_packet = SHandshake { session_id: 1 };
assert_eq!(packet, actual_packet);
}
#[tokio::test]
async fn test_handshake_response() {
let bytes = vec![
0x09, 0x00, 0x00, 0x00, 0x01, 0x39, 0x35, 0x31, 0x33, 0x33, 0x30, 0x37, 0x00,
];
let packet = CHandshake {
session_id: 1,
challange_token: 9513307,
};
assert_eq!(bytes, packet.encode().await)
}
#[tokio::test]
async fn test_basic_stat_request() {
let bytes = vec![
0xFE, 0xFD, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x91, 0x29, 0x5B,
];
let mut raw_packet = RawQueryPacket::decode(bytes).await.unwrap();
let packet = SStatusRequest::decode(&mut raw_packet).await.unwrap();
let actual_packet = SStatusRequest {
session_id: 1,
challange_token: 9513307,
is_full_request: false,
};
assert_eq!(packet, actual_packet);
}
#[tokio::test]
async fn test_basic_stat_response() {
let bytes = 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 packet = CBasicStatus {
session_id: 1,
motd: CString::new("A Minecraft Server").unwrap(),
map: CString::new("world").unwrap(),
num_players: 2,
max_players: 20,
host_port: 25565,
host_ip: CString::new("127.0.0.1").unwrap(),
};
assert_eq!(bytes, packet.encode().await);
}
#[tokio::test]
async fn test_full_stat_request() {
let bytes = vec![
0xFE, 0xFD, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x91, 0x29, 0x5B, 0x00, 0x00, 0x00, 0x00,
];
let mut raw_packet = RawQueryPacket::decode(bytes).await.unwrap();
let packet = SStatusRequest::decode(&mut raw_packet).await.unwrap();
let actual_packet = SStatusRequest {
session_id: 1,
challange_token: 9513307,
is_full_request: true,
};
assert_eq!(packet, actual_packet);
}
#[tokio::test]
async fn test_full_stat_response() {
let bytes = 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 packet = CFullStatus {
session_id: 1,
hostname: CString::new("A Minecraft Server").unwrap(),
version: CString::new("Beta 1.9 Prerelease 4").unwrap(),
plugins: CString::new("").unwrap(),
map: CString::new("world").unwrap(),
num_players: 2,
max_players: 20,
host_port: 25565,
host_ip: CString::new("127.0.0.1").unwrap(),
players: vec![
CString::new("barneygale").unwrap(),
CString::new("Vivalahelvig").unwrap(),
],
};
assert_eq!(bytes, packet.encode().await);
}

View File

@@ -42,6 +42,7 @@ pub mod command;
pub mod entity; pub mod entity;
pub mod error; pub mod error;
pub mod proxy; pub mod proxy;
pub mod query;
pub mod rcon; pub mod rcon;
pub mod server; pub mod server;
pub mod world; pub mod world;
@@ -115,10 +116,13 @@ async fn main() -> io::Result<()> {
let time = Instant::now(); let time = Instant::now();
// Setup the TCP server socket. // Setup the TCP server socket.
let addr = BASIC_CONFIG.server_address; let listener = tokio::net::TcpListener::bind(BASIC_CONFIG.server_address)
let listener = tokio::net::TcpListener::bind(addr)
.await .await
.expect("Failed to start TcpListener"); .expect("Failed to start TcpListener");
// In the event the user puts 0 for their port, this will allow us to know what port it is running on
let addr = listener
.local_addr()
.expect("Unable to get the address of server!");
let use_console = ADVANCED_CONFIG.commands.use_console; let use_console = ADVANCED_CONFIG.commands.use_console;
let rcon = ADVANCED_CONFIG.rcon.clone(); let rcon = ADVANCED_CONFIG.rcon.clone();
@@ -138,6 +142,12 @@ async fn main() -> io::Result<()> {
RCONServer::new(&rcon, server).await.unwrap(); RCONServer::new(&rcon, server).await.unwrap();
}); });
} }
if ADVANCED_CONFIG.query.enabled {
log::info!("Query protocol enabled. Starting...");
tokio::spawn(query::start_query_handler(server.clone(), addr));
}
{ {
let server = server.clone(); let server = server.clone();
tokio::spawn(async move { tokio::spawn(async move {

173
pumpkin/src/query.rs Normal file
View File

@@ -0,0 +1,173 @@
use std::{
collections::HashMap,
ffi::{CString, NulError},
net::SocketAddr,
sync::Arc,
time::Duration,
};
use pumpkin_config::{ADVANCED_CONFIG, BASIC_CONFIG};
use pumpkin_protocol::query::{
CBasicStatus, CFullStatus, CHandshake, PacketType, RawQueryPacket, SHandshake, SStatusRequest,
};
use rand::Rng;
use tokio::{net::UdpSocket, sync::RwLock, time};
use crate::server::{Server, CURRENT_MC_VERSION};
pub async fn start_query_handler(server: Arc<Server>, bound_addr: SocketAddr) {
let mut query_addr = bound_addr;
if let Some(port) = ADVANCED_CONFIG.query.port {
query_addr.set_port(port);
}
let socket = Arc::new(
UdpSocket::bind(query_addr)
.await
.expect("Unable to bind to address"),
);
// Challange tokens are bound to the IP address and port
let valid_challange_tokens = Arc::new(RwLock::new(HashMap::new()));
let valid_challange_tokens_clone = valid_challange_tokens.clone();
// All challange tokens ever created are expired every 30 seconds
tokio::spawn(async move {
let mut interval = time::interval(Duration::from_secs(30));
loop {
interval.tick().await;
valid_challange_tokens_clone.write().await.clear();
}
});
log::info!(
"Server query running on {}",
socket
.local_addr()
.expect("Unable to find running address!")
);
loop {
let socket = socket.clone();
let valid_challange_tokens = valid_challange_tokens.clone();
let server = server.clone();
let mut buf = vec![0; 1024];
let (_, addr) = socket.recv_from(&mut buf).await.unwrap();
tokio::spawn(async move {
if let Err(err) = handle_packet(
buf,
valid_challange_tokens,
server,
socket,
addr,
bound_addr,
)
.await
{
log::error!("Interior 0 bytes found! Cannot encode query response! {err}");
}
});
}
}
// Errors of packets that don't meet the format aren't returned since we won't handle them anyway
// The only errors that are thrown are because of a null terminator in a CString
// since those errors need to be corrected by server owner
#[inline]
async fn handle_packet(
buf: Vec<u8>,
clients: Arc<RwLock<HashMap<i32, SocketAddr>>>,
server: Arc<Server>,
socket: Arc<UdpSocket>,
addr: SocketAddr,
bound_addr: SocketAddr,
) -> Result<(), NulError> {
if let Ok(mut raw_packet) = RawQueryPacket::decode(buf).await {
match raw_packet.packet_type {
PacketType::Handshake => {
if let Ok(packet) = SHandshake::decode(&mut raw_packet).await {
let challange_token = rand::thread_rng().gen_range(1..=i32::MAX);
let response = CHandshake {
session_id: packet.session_id,
challange_token,
};
// Ignore all errors since we don't want the query handler to crash
// Protocol also ignores all errors and just doesn't respond
let _ = socket
.send_to(response.encode().await.as_slice(), addr)
.await;
clients.write().await.insert(challange_token, addr);
}
}
PacketType::Status => {
if let Ok(packet) = SStatusRequest::decode(&mut raw_packet).await {
if clients
.read()
.await
.get(&packet.challange_token)
.is_some_and(|token_bound_ip: &SocketAddr| token_bound_ip == &addr)
{
if packet.is_full_request {
// Get 4 players
let mut players: Vec<CString> = Vec::new();
for world in &server.worlds {
let mut world_players = world
.current_players
.lock()
.await
// Although there is no documented limit, we will limit to 4 players
.values()
.take(4 - players.len())
.map(|player| {
CString::new(player.gameprofile.name.as_str()).unwrap()
})
.collect::<Vec<_>>();
players.append(&mut world_players); // Append players from this world
if players.len() >= 4 {
break; // Stop if we've collected 4 players
}
}
let response = CFullStatus {
session_id: packet.session_id,
hostname: CString::new(BASIC_CONFIG.motd.as_str())?,
version: CString::new(CURRENT_MC_VERSION)?,
plugins: CString::new("Pumpkin on 1.21.3")?, // TODO: Fill this with plugins when plugins are working
map: CString::new("world")?, // TODO: Get actual world name
num_players: server.get_player_count().await,
max_players: BASIC_CONFIG.max_players as usize,
host_port: bound_addr.port(),
host_ip: CString::new(bound_addr.ip().to_string())?,
players,
};
let _ = socket
.send_to(response.encode().await.as_slice(), addr)
.await;
} else {
let resposne = CBasicStatus {
session_id: packet.session_id,
motd: CString::new(BASIC_CONFIG.motd.as_str())?,
map: CString::new("world")?,
num_players: server.get_player_count().await,
max_players: BASIC_CONFIG.max_players as usize,
host_port: bound_addr.port(),
host_ip: CString::new(bound_addr.ip().to_string())?,
};
let _ = socket
.send_to(resposne.encode().await.as_slice(), addr)
.await;
}
}
}
}
}
}
Ok(())
}