From 414cf3190ccc36e4f66e3e4d3c395245ff141b5b Mon Sep 17 00:00:00 2001 From: Snowiiii Date: Sat, 10 Aug 2024 23:53:32 +0200 Subject: [PATCH] Added RCON support --- Cargo.lock | 1 + README.md | 10 +- pumpkin-registry/src/chat_type.rs | 2 +- pumpkin-text/src/lib.rs | 2 +- pumpkin/Cargo.toml | 2 + pumpkin/src/client/authentication.rs | 4 +- pumpkin/src/client/player_packet.rs | 7 +- pumpkin/src/commands/mod.rs | 7 +- pumpkin/src/config/mod.rs | 23 ++- pumpkin/src/main.rs | 11 +- pumpkin/src/rcon/mod.rs | 220 +++++++++++++++++++++++++++ pumpkin/src/rcon/packet.rs | 113 ++++++++++++++ 12 files changed, 386 insertions(+), 16 deletions(-) create mode 100644 pumpkin/src/rcon/mod.rs create mode 100644 pumpkin/src/rcon/packet.rs diff --git a/Cargo.lock b/Cargo.lock index 65dfa29a1..2475436a4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1154,6 +1154,7 @@ name = "pumpkin" version = "0.1.0-dev" dependencies = [ "base64", + "bytes", "crossbeam-channel", "digest 0.11.0-pre.9", "image 0.25.2", diff --git a/README.md b/README.md index 7ca197778..505e303bd 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ ![CI](https://github.com/Snowiiii/Pumpkin/actions/workflows/rust.yml/badge.svg) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) -![Current version)](https://img.shields.io/badge/current_version-1.21-blue) +![Current version)](https://img.shields.io/badge/current_version-1.21.1-blue) @@ -27,13 +27,10 @@ Pumpkin is currently under heavy development. ### Features (WIP) - [x] Configuration (toml) - [x] Server Status/Ping - - [x] Custom maximum player amout + - [x] Custom maximum player amount - [x] Custom Icon - [x] Custom Status (MOTD) -- Login - - [x] Authentication - - [x] Encryption - - [x] Packet Compression +- [x] Login - Player Configuration - [x] Registries (biome types, paintings, dimensions) - [x] Server Brand @@ -57,6 +54,7 @@ Pumpkin is currently under heavy development. - [ ] Player Inventory - [ ] Player Attack - Server + - [x] RCON - [x] Inventories - [x] Chat - [x] Commands diff --git a/pumpkin-registry/src/chat_type.rs b/pumpkin-registry/src/chat_type.rs index aa70c24bb..a66dc7647 100644 --- a/pumpkin-registry/src/chat_type.rs +++ b/pumpkin-registry/src/chat_type.rs @@ -2,6 +2,6 @@ pub struct ChatType {} pub struct Decoration { translation_key: String, - // style: Option, + // style: Option, parameters: Vec, } diff --git a/pumpkin-text/src/lib.rs b/pumpkin-text/src/lib.rs index 1a9512bdc..f18c04188 100644 --- a/pumpkin-text/src/lib.rs +++ b/pumpkin-text/src/lib.rs @@ -231,4 +231,4 @@ impl Default for TextContent { fn default() -> Self { Self::Text { text: "".into() } } -} \ No newline at end of file +} diff --git a/pumpkin/Cargo.toml b/pumpkin/Cargo.toml index 4288e8fef..b4b956829 100644 --- a/pumpkin/Cargo.toml +++ b/pumpkin/Cargo.toml @@ -17,6 +17,8 @@ serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" toml = "0.8.19" +bytes = "1.7" + rand = "0.8.5" num-traits = "0.2" diff --git a/pumpkin/src/client/authentication.rs b/pumpkin/src/client/authentication.rs index 2de7a4b20..e4aa33996 100644 --- a/pumpkin/src/client/authentication.rs +++ b/pumpkin/src/client/authentication.rs @@ -1,9 +1,9 @@ -use std::{collections::HashMap, net::IpAddr, time::Duration}; +use std::{collections::HashMap, net::IpAddr}; use base64::{engine::general_purpose, Engine}; use num_bigint::BigInt; use pumpkin_protocol::Property; -use reqwest::{header::CONTENT_TYPE, StatusCode, Url}; +use reqwest::{StatusCode, Url}; use serde::{Deserialize, Serialize}; use thiserror::Error; use uuid::Uuid; diff --git a/pumpkin/src/client/player_packet.rs b/pumpkin/src/client/player_packet.rs index 95d429338..b020b4d4c 100644 --- a/pumpkin/src/client/player_packet.rs +++ b/pumpkin/src/client/player_packet.rs @@ -8,8 +8,9 @@ use pumpkin_protocol::{ server::play::{ SChatCommand, SChatMessage, SConfirmTeleport, SPlayerCommand, SPlayerPosition, SPlayerPositionRotation, SPlayerRotation, SSwingArm, - }, VarInt, - }; + }, + VarInt, +}; use pumpkin_text::TextComponent; use crate::{ @@ -156,7 +157,7 @@ impl Client { } pub fn handle_chat_command(&mut self, _server: &mut Server, command: SChatCommand) { - handle_command(&mut CommandSender::Player(self), command.command); + handle_command(&mut CommandSender::Player(self), &command.command); } pub fn handle_player_command(&mut self, _server: &mut Server, command: SPlayerCommand) { diff --git a/pumpkin/src/commands/mod.rs b/pumpkin/src/commands/mod.rs index ba095c710..3e75202a2 100644 --- a/pumpkin/src/commands/mod.rs +++ b/pumpkin/src/commands/mod.rs @@ -23,6 +23,7 @@ pub trait Command<'a> { } pub enum CommandSender<'a> { + Rcon(&'a mut Vec), Console, Player(&'a mut Client), } @@ -33,6 +34,7 @@ impl<'a> CommandSender<'a> { // TODO: add color and stuff to console CommandSender::Console => log::info!("{:?}", text.content), CommandSender::Player(c) => c.send_system_message(text), + CommandSender::Rcon(s) => s.push(format!("{:?}", text.content)), } } @@ -40,6 +42,7 @@ impl<'a> CommandSender<'a> { match self { CommandSender::Console => false, CommandSender::Player(_) => true, + CommandSender::Rcon(_) => false, } } @@ -47,16 +50,18 @@ impl<'a> CommandSender<'a> { match self { CommandSender::Console => true, CommandSender::Player(_) => false, + CommandSender::Rcon(_) => true, } } pub fn as_mut_player(&mut self) -> Option<&mut Client> { match self { CommandSender::Player(client) => Some(client), CommandSender::Console => None, + CommandSender::Rcon(_) => None, } } } -pub fn handle_command(sender: &mut CommandSender, command: String) { +pub fn handle_command(sender: &mut CommandSender, command: &str) { let command = command.to_lowercase(); // an ugly mess i know if command.starts_with(PumpkinCommand::NAME) { diff --git a/pumpkin/src/config/mod.rs b/pumpkin/src/config/mod.rs index 98779ec4a..ee84669e0 100644 --- a/pumpkin/src/config/mod.rs +++ b/pumpkin/src/config/mod.rs @@ -21,6 +21,26 @@ pub struct AdvancedConfiguration { pub authentication: Authentication, pub packet_compression: Compression, pub resource_pack: ResourcePack, + pub rcon: RCONConfig, +} + +#[derive(Deserialize, Serialize, Clone)] +pub struct RCONConfig { + pub enabled: bool, + pub ip: String, + pub port: u16, + pub password: String, +} + +impl Default for RCONConfig { + fn default() -> Self { + Self { + enabled: false, + ip: "0.0.0.0".to_string(), + port: 25575, + password: "".to_string(), + } + } } #[derive(Deserialize, Serialize)] @@ -67,6 +87,7 @@ impl Default for AdvancedConfiguration { commands: Commands::default(), packet_compression: Compression::default(), resource_pack: ResourcePack::default(), + rcon: RCONConfig::default(), } } } @@ -107,7 +128,7 @@ impl Default for BasicConfiguration { fn default() -> Self { Self { config_version: CURRENT_BASE_VERSION.to_string(), - server_address: "127.0.0.1".to_string(), + server_address: "0.0.0.0".to_string(), server_port: 25565, seed: "".to_string(), max_players: 100000, diff --git a/pumpkin/src/main.rs b/pumpkin/src/main.rs index 2746444a6..2eb6a1bc3 100644 --- a/pumpkin/src/main.rs +++ b/pumpkin/src/main.rs @@ -19,6 +19,7 @@ pub mod client; pub mod commands; pub mod config; pub mod entity; +pub mod rcon; pub mod server; pub mod util; @@ -27,6 +28,8 @@ pub mod util; async fn main() -> io::Result<()> { use std::{cell::RefCell, time::Instant}; + use rcon::RCONServer; + let time = Instant::now(); let basic_config = BasicConfiguration::load("configuration.toml"); @@ -58,6 +61,7 @@ async fn main() -> io::Result<()> { let mut unique_token = Token(SERVER.0 + 1); let use_console = advanced_configuration.commands.use_console; + let rcon = advanced_configuration.rcon.clone(); let mut connections: HashMap>> = HashMap::new(); @@ -73,10 +77,15 @@ async fn main() -> io::Result<()> { stdin .read_line(&mut out) .expect("Failed to read console line"); - handle_command(&mut commands::CommandSender::Console, out); + handle_command(&mut commands::CommandSender::Console, &out); } }); } + if rcon.enabled { + tokio::spawn(async move { + RCONServer::new(&rcon).await.unwrap(); + }); + } loop { if let Err(err) = poll.poll(&mut events, None) { if interrupted(&err) { diff --git a/pumpkin/src/rcon/mod.rs b/pumpkin/src/rcon/mod.rs new file mode 100644 index 000000000..c3606ca2e --- /dev/null +++ b/pumpkin/src/rcon/mod.rs @@ -0,0 +1,220 @@ +use std::{ + collections::HashMap, + io::{self, Read}, +}; + +use mio::{ + net::{TcpListener, TcpStream}, + Events, Interest, Poll, Token, +}; +use packet::{Packet, PacketError, PacketType}; +use thiserror::Error; + +use crate::{commands::handle_command, config::RCONConfig}; + +mod packet; + +#[derive(Debug, Error)] +pub enum RCONError { + #[error("authentication failed")] + Auth, + #[error("command exceeds the maximum length")] + CommandTooLong, + #[error("{}", _0)] + Io(io::Error), +} + +const SERVER: Token = Token(0); + +pub struct RCONServer {} + +impl RCONServer { + pub async fn new(config: &RCONConfig) -> Result { + assert!(config.enabled, "RCON is not enabled"); + let addr = format!("{}:{}", config.ip, config.port) + .parse() + .expect("Failed to parse RCON address"); + let mut poll = Poll::new().unwrap(); + let mut listener = TcpListener::bind(addr).unwrap(); + + poll.registry() + .register(&mut listener, SERVER, Interest::READABLE) + .unwrap(); + + let mut unique_token = Token(SERVER.0 + 1); + + let mut events = Events::with_capacity(128); + + let mut connections: HashMap = HashMap::new(); + + let password = config.password.clone(); + + loop { + poll.poll(&mut events, None).unwrap(); + + for event in events.iter() { + match event.token() { + SERVER => loop { + // Received an event for the TCP server socket, which + // indicates we can accept an connection. + let (mut connection, address) = match listener.accept() { + Ok((connection, address)) => (connection, address), + Err(e) if e.kind() == io::ErrorKind::WouldBlock => { + // If we get a `WouldBlock` error we know our + // listener has no more incoming connections queued, + // so we can return to polling and wait for some + // more. + break; + } + Err(e) => { + // If it was any other kind of error, something went + // wrong and we terminate with an error. + return Err(e); + } + }; + log::info!("Accepted connection from: {}", address); + + let token = Self::next(&mut unique_token); + poll.registry() + .register( + &mut connection, + token, + Interest::READABLE.add(Interest::WRITABLE), + ) + .unwrap(); + connections.insert(token, RCONClient::new(connection)); + }, + + token => { + let done = if let Some(client) = connections.get_mut(&token) { + client.handle(&password).await + } else { + false + }; + if done { + if let Some(mut client) = connections.remove(&token) { + poll.registry().deregister(&mut client.connection)?; + } + } + } + } + } + } + } + + fn next(current: &mut Token) -> Token { + let next = current.0; + current.0 += 1; + Token(next) + } +} + +pub struct RCONClient { + connection: TcpStream, + logged_in: bool, + incoming: Vec, + closed: bool, +} + +impl RCONClient { + pub fn new(connection: TcpStream) -> Self { + Self { + connection, + logged_in: false, + incoming: Vec::new(), + closed: false, + } + } + + pub async fn handle(&mut self, password: &str) -> bool { + if !self.closed { + loop { + match self.read_bytes() { + // Stream closed, so we can't reply, so we just close everything. + Ok(true) => return true, + Ok(false) => {} + Err(e) if e.kind() == io::ErrorKind::WouldBlock => break, + Err(e) => { + log::error!("could not read packet: {e}"); + return true; + } + } + } + // If we get a close here, we might have a reply, which we still want to write. + match self.poll(password).await { + Ok(()) => {} + Err(e) => { + log::error!("rcon error: {e}"); + self.closed = true; + } + } + } + self.closed + } + + async fn poll(&mut self, password: &str) -> Result<(), PacketError> { + loop { + let packet = match self.receive_packet().await? { + Some(p) => p, + None => return Ok(()), + }; + + match packet.get_type() { + PacketType::Auth => { + let body = packet.get_body(); + if !body.is_empty() && packet.get_body() == password { + self.send(&mut Packet::new( + packet.get_id(), + PacketType::AuthResponse, + "".into(), + )) + .await + .unwrap(); + log::info!("RCON Client logged in successfully"); + self.logged_in = true; + } else { + log::warn!("RCON Client has tried wrong password"); + self.send(&mut Packet::new(-1, PacketType::AuthResponse, "".into())) + .await + .unwrap(); + return Err(PacketError::WrongPassword); + } + } + PacketType::ExecCommand => { + if self.logged_in { + let mut output = Vec::new(); + handle_command( + &mut crate::commands::CommandSender::Rcon(&mut output), + packet.get_body(), + ); + for line in output { + self.send(&mut Packet::new(packet.get_id(), PacketType::Output, line)) + .await + .unwrap(); + } + } + } + PacketType::Output => todo!(), + PacketType::AuthResponse => unreachable!(), + } + } + } + + fn read_bytes(&mut self) -> io::Result { + let mut buf = [0; 1460]; + let n = self.connection.read(&mut buf)?; + if n == 0 { + return Ok(true); + } + self.incoming.extend_from_slice(&buf[..n]); + Ok(false) + } + + async fn send(&mut self, packet: &mut Packet) -> io::Result<()> { + packet.send_packet(&mut self.connection).await + } + + async fn receive_packet(&mut self) -> Result, PacketError> { + Packet::deserialize(&mut self.incoming).await + } +} diff --git a/pumpkin/src/rcon/packet.rs b/pumpkin/src/rcon/packet.rs new file mode 100644 index 000000000..5d1abd195 --- /dev/null +++ b/pumpkin/src/rcon/packet.rs @@ -0,0 +1,113 @@ +use std::io::{self, BufRead, Cursor, Write}; + +use bytes::{BufMut, BytesMut}; +use mio::net::TcpStream; +use thiserror::Error; +use tokio::io::{AsyncReadExt, AsyncWriteExt}; + +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum PacketType { + Auth, + AuthResponse, + ExecCommand, + Output, +} + +#[derive(Error, Debug)] +pub enum PacketError { + #[error("invalid length")] + InvalidLength, + #[error("expected terminating NUL byte")] + NoNullTermination, + #[error("wrong password")] + WrongPassword, +} + +impl PacketType { + fn to_i32(self) -> i32 { + match self { + PacketType::Auth => 3, + PacketType::AuthResponse => 2, + PacketType::ExecCommand => 2, + PacketType::Output => 0, + } + } + + pub fn from_i32(n: i32) -> PacketType { + match n { + 3 => PacketType::Auth, + 2 => PacketType::ExecCommand, + _ => PacketType::Output, + } + } +} + +#[derive(Debug)] +pub struct Packet { + id: i32, + ptype: PacketType, + body: String, +} + +impl Packet { + pub fn new(id: i32, ptype: PacketType, body: String) -> Packet { + Packet { id, ptype, body } + } + + pub async fn send_packet(&mut self, connection: &mut TcpStream) -> io::Result<()> { + // let len = outgoing.len() as u64; + let mut buf = BytesMut::new(); + // 10 is for 4 bytes ty, 4 bytes id, and 2 terminating nul bytes. + buf.put_i32_le(10 + self.get_body().len() as i32); + buf.put_i32_le(self.id); + buf.put_i32_le(self.get_type().to_i32()); + let bytes = self.get_body().as_bytes(); + buf.put_slice(bytes); + buf.put_u8(0); + buf.put_u8(0); + connection.write(&buf).unwrap(); + Ok(()) + } + + pub async fn deserialize(incoming: &mut Vec) -> Result, PacketError> { + if incoming.len() < 4 { + return Ok(None); + } + let mut buf = Cursor::new(&incoming); + let len = buf.read_i32_le().await.unwrap() + 4; + if !(0..=1460).contains(&len) { + return Err(PacketError::InvalidLength); + } + let id = buf.read_i32_le().await.unwrap(); + let ty = buf.read_i32_le().await.unwrap(); + let mut payload = vec![]; + let _ = buf.read_until(b'\0', &mut payload).unwrap(); + payload.pop(); + if buf.read_u8().await.unwrap() != 0 { + return Err(PacketError::NoNullTermination); + } + if buf.position() != len as u64 { + return Err(PacketError::InvalidLength); + } + incoming.drain(0..len as usize); + + let packet = Packet { + id, + ptype: PacketType::from_i32(ty), + body: String::from_utf8(payload).unwrap(), + }; + + Ok(Some(packet)) + } + pub fn get_body(&self) -> &str { + &self.body + } + + pub fn get_type(&self) -> PacketType { + self.ptype + } + + pub fn get_id(&self) -> i32 { + self.id + } +}