also add contributors to /pumpkin command Co-Authored-By: Fyor <90567841+fyordev@users.noreply.github.com> Co-Authored-By: MartV0 <34748938+martv0@users.noreply.github.com> Co-Authored-By: FabseGP <fabsegp@fabseman.space>
363 lines
12 KiB
Rust
363 lines
12 KiB
Rust
use std::{ffi::CString, io::Cursor};
|
|
|
|
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
|
|
|
const PADDING_START: [u8; 11] = [
|
|
0x73, 0x70, 0x6C, 0x69, 0x74, 0x6E, 0x75, 0x6D, 0x00, 0x80, 0x00,
|
|
];
|
|
|
|
const PADDING_END: [u8; 11] = [
|
|
0x00, 0x01, 0x70, 0x6C, 0x61, 0x79, 0x65, 0x72, 0x5F, 0x00, 0x00,
|
|
];
|
|
|
|
pub enum PacketType {
|
|
// There could be other types, but they are not documented.
|
|
// Besides, these types are enough to get the server status.
|
|
Handshake = 9,
|
|
Status = 0,
|
|
}
|
|
|
|
pub struct InvalidPacketType;
|
|
|
|
impl TryFrom<u8> for PacketType {
|
|
type Error = InvalidPacketType;
|
|
|
|
fn try_from(value: u8) -> Result<Self, Self::Error> {
|
|
match value {
|
|
9 => Ok(Self::Handshake),
|
|
0 => Ok(Self::Status),
|
|
_ => Err(InvalidPacketType),
|
|
}
|
|
}
|
|
}
|
|
|
|
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 protocol being used.
|
|
// We should not attempt to decode packets with other magic values.
|
|
65277 => Ok(Self {
|
|
packet_type: PacketType::try_from(reader.read_u8().await.map_err(|_| ())?)
|
|
.map_err(|_| ())?,
|
|
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 challenge_token: i32,
|
|
// A full status request and a basic status request are pretty similar,
|
|
// so we might as well 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(|_| ())?,
|
|
challenge_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 ignore malformed packets or errors
|
|
return Err(());
|
|
}
|
|
}
|
|
},
|
|
})
|
|
}
|
|
}
|
|
|
|
pub struct CHandshake {
|
|
pub session_id: i32,
|
|
// For simplicity, use a number type.
|
|
// It should be encoded as string here;
|
|
// it will be converted in encoding.
|
|
pub challenge_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();
|
|
// Challenge token
|
|
// Use CString to add null terminator and ensure no null bytes are in the middle of the data
|
|
// Unwrap here since there should be no errors with nulls in the middle of data
|
|
let token = CString::new(self.challenge_token.to_string()).unwrap();
|
|
buf.extend_from_slice(token.as_bytes_with_nul());
|
|
|
|
buf
|
|
}
|
|
}
|
|
|
|
pub struct CBasicStatus {
|
|
pub session_id: i32,
|
|
// Use CString, as the 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 the 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, it seems in testing some query checkers depend on these bytes?
|
|
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
|
|
|
|
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,
|
|
challenge_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,
|
|
challenge_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,
|
|
challenge_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);
|
|
}
|