mirror of
https://github.com/Pumpkin-MC/Pumpkin.git
synced 2026-08-30 20:14:23 +00:00
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:
committed by
GitHub
parent
1f8385e890
commit
93c6da993f
@@ -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
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|||||||
12
pumpkin-config/src/query.rs
Normal file
12
pumpkin-config/src/query.rs
Normal 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>,
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
350
pumpkin-protocol/src/query.rs
Normal file
350
pumpkin-protocol/src/query.rs
Normal 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);
|
||||||
|
}
|
||||||
@@ -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
173
pumpkin/src/query.rs
Normal 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(())
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user