Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement Query protocol #218

Merged
merged 26 commits into from
Nov 5, 2024
Merged
Show file tree
Hide file tree
Changes from 25 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
813ef02
Add query options to config
neeleshpoli Oct 31, 2024
363e06b
Run cargo fmt
neeleshpoli Oct 31, 2024
5285361
Add repr(u8) so that PacketType is a byte
neeleshpoli Oct 31, 2024
83cb826
Respond to handshake correctly
neeleshpoli Nov 1, 2024
9b8dfa1
Run cargo fmt
neeleshpoli Nov 1, 2024
1c19a3e
Merge branch 'Snowiiii:master' into master
neeleshpoli Nov 1, 2024
2e2d040
Allow for proper decoding of status packets and properly handle error…
neeleshpoli Nov 1, 2024
057d2be
Implement challange tokens and verify with token and address
neeleshpoli Nov 1, 2024
93093f6
Encode Basic status packet
neeleshpoli Nov 1, 2024
09a742a
Encode Full status packet
neeleshpoli Nov 1, 2024
80bc977
Add comment, run cargo fmt
neeleshpoli Nov 1, 2024
ffa7c3a
Merge branch 'Snowiiii:master' into master
neeleshpoli Nov 1, 2024
bab7bdc
Merge branch 'master' of https://github.com/neeleshpoli/Pumpkin
neeleshpoli Nov 1, 2024
bb7256a
Add forgotten fields to full status packet
neeleshpoli Nov 1, 2024
f38caf5
Correctly respond to query clients when requesting full status
neeleshpoli Nov 2, 2024
572aad2
Run cargo fmt, remove unused imports
neeleshpoli Nov 2, 2024
399c056
Fix clippy suggestions
neeleshpoli Nov 2, 2024
0cc2872
Return actual address and port of server
neeleshpoli Nov 2, 2024
bbb1246
Remove uncessary clone
neeleshpoli Nov 2, 2024
a79669e
Remove packet type as it is redundant/unnecssary
neeleshpoli Nov 2, 2024
795f2e4
Cargo fmt
neeleshpoli Nov 2, 2024
c5aac53
Implement basic status request
neeleshpoli Nov 3, 2024
3f8e3a3
Merge branch 'Snowiiii:master' into master
neeleshpoli Nov 3, 2024
7b86229
Update README
neeleshpoli Nov 3, 2024
5683a68
Show players correctly in full status packet
neeleshpoli Nov 3, 2024
70ace3d
Store all packets in structs instead of enums
neeleshpoli Nov 5, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ and customizable experience. It prioritizes performance and player enjoyment whi
- [x] Player Combat
- Server
- [ ] Plugins
- [ ] Query
- [x] Query
- [x] RCON
- [x] Inventories
- [x] Particles
Expand Down
3 changes: 3 additions & 0 deletions pumpkin-config/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use log::warn;
use logging::LoggingConfig;
use pumpkin_core::{Difficulty, GameMode};
use query::QueryConfig;
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
Expand All @@ -16,6 +17,7 @@ use std::{
pub mod auth;
pub mod logging;
pub mod proxy;
pub mod query;
pub mod resource_pack;

pub use auth::AuthenticationConfig;
Expand Down Expand Up @@ -53,6 +55,7 @@ pub struct AdvancedConfiguration {
pub rcon: RCONConfig,
pub pvp: PVPConfig,
pub logging: LoggingConfig,
pub query: QueryConfig,
}

#[serde_inline_default]
Expand Down
12 changes: 12 additions & 0 deletions pumpkin-config/src/query.rs
Original file line number Diff line number Diff line change
@@ -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>,
}
2 changes: 1 addition & 1 deletion pumpkin-protocol/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ serde.workspace = true
thiserror.workspace = true
itertools.workspace = true
log.workspace = true

tokio.workspace = true
neeleshpoli marked this conversation as resolved.
Show resolved Hide resolved
num-traits.workspace = true
num-derive.workspace = true

Expand Down
1 change: 1 addition & 0 deletions pumpkin-protocol/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ pub mod bytebuf;
pub mod client;
pub mod packet_decoder;
pub mod packet_encoder;
pub mod query;
pub mod server;
pub mod slot;

Expand Down
213 changes: 213 additions & 0 deletions pumpkin-protocol/src/query.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
use std::ffi::CString;

use tokio::io::{AsyncReadExt, AsyncWriteExt};

pub struct SBasePacket {
pub magic: u16,
neeleshpoli marked this conversation as resolved.
Show resolved Hide resolved
pub session_id: i32,
pub payload: SBasePayload,
}

pub enum SBasePayload {
Handshake,
BasicInfo(i32),
FullInfo(i32),
}

impl SBasePacket {
// We don't care what error it is as any packet with errors will be ingnored
pub async fn decode(mut reader: impl AsyncReadExt + Unpin) -> Result<Self, ()> {
let magic = reader.read_u16().await.map_err(|_| ())?;
neeleshpoli marked this conversation as resolved.
Show resolved Hide resolved
let packet_type = reader.read_u8().await.map_err(|_| ())?;
let session_id = reader.read_i32().await.map_err(|_| ())?;

match packet_type {
// Status
0 => {
neeleshpoli marked this conversation as resolved.
Show resolved Hide resolved
let challange_token = reader.read_i32().await.map_err(|_| ())?;
neeleshpoli marked this conversation as resolved.
Show resolved Hide resolved
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 reader.read(&mut buf).await {
Ok(0) => Ok(Self {
magic,
session_id,
payload: SBasePayload::BasicInfo(challange_token),
}),
Ok(4) => Ok(Self {
magic,
session_id,
payload: SBasePayload::FullInfo(challange_token),
}),
_ => {
// Just ingnore malformed packets or errors
Err(())
}
}
}

// Handshake
9 => Ok(Self {
magic,
session_id,
payload: SBasePayload::Handshake,
}),

_ => Err(()),
}
}
}

pub struct CBasePacket {
pub session_id: i32,
pub payload: CBasePayload,
}

#[derive(Debug)]
pub enum CBasePayload {
Handshake {
// For simplicity use a number type
// Should be encoded as string here
// Will be converted in encoding
challange_token: i32,
},
BasicInfo {
// Use CString as protocol requires nul terminated strings
motd: CString,
// Game type is hardcoded
map: CString,
num_players: usize,
max_players: usize,
host_port: u16,
host_ip: CString,
},
FullInfo {
hostname: CString,
// Game type and game id are hardcoded into protocol
// They are not here as they cannot be changed
version: CString,
plugins: CString,
map: CString,
num_players: usize,
max_players: usize,
host_port: u16,
host_ip: CString,
players: Vec<CString>,
},
}
neeleshpoli marked this conversation as resolved.
Show resolved Hide resolved

impl CBasePacket {
pub async fn encode(&self) -> Vec<u8> {
let mut buf = Vec::new();

match &self.payload {
CBasePayload::Handshake { challange_token } => {
// 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(challange_token.to_string()).unwrap();
buf.extend_from_slice(token.as_bytes_with_nul());
}
CBasePayload::BasicInfo {
motd,
map,
num_players,
max_players,
host_port,
host_ip,
} => {
// Packet Type
buf.write_u8(0).await.unwrap();
// Session ID
buf.write_i32(self.session_id).await.unwrap();
// MOTD
buf.extend_from_slice(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(map.as_bytes_with_nul());
// Num players
let num_players = CString::new(num_players.to_string()).unwrap();
buf.extend_from_slice(num_players.as_bytes_with_nul());
// Max players
let max_players = CString::new(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(*host_port).await.unwrap();
// IP
buf.extend_from_slice(host_ip.as_bytes_with_nul());
}
CBasePayload::FullInfo {
hostname,
version,
plugins,
map,
num_players,
max_players,
host_port,
host_ip,
players,
} => {
// 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
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", hostname),
("gametype", &CString::new("SMP").unwrap()),
("game_id", &CString::new("MINECRAFT").unwrap()),
neeleshpoli marked this conversation as resolved.
Show resolved Hide resolved
("version", version),
("plugins", plugins),
("map", map),
(
"numplayers",
&CString::new(num_players.to_string()).unwrap(),
),
(
"maxplayers",
&CString::new(max_players.to_string()).unwrap(),
),
("hostport", &CString::new(host_port.to_string()).unwrap()),
("hostip", 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 players {
buf.extend_from_slice(player.as_bytes_with_nul());
}
// Required extra null terminator
buf.write_u8(0).await.unwrap();
}
}

buf
}
}
14 changes: 12 additions & 2 deletions pumpkin/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ pub mod command;
pub mod entity;
pub mod error;
pub mod proxy;
pub mod query;
pub mod rcon;
pub mod server;
pub mod world;
Expand Down Expand Up @@ -117,10 +118,13 @@ async fn main() -> io::Result<()> {
let time = Instant::now();

// Setup the TCP server socket.
let addr = BASIC_CONFIG.server_address;
let listener = tokio::net::TcpListener::bind(addr)
let listener = tokio::net::TcpListener::bind(BASIC_CONFIG.server_address)
.await
.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 rcon = ADVANCED_CONFIG.rcon.clone();
Expand All @@ -140,6 +144,12 @@ async fn main() -> io::Result<()> {
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();
tokio::spawn(async move {
Expand Down
Loading