From ae7ef10b67737f08a86743f687bca30a12e9e17a Mon Sep 17 00:00:00 2001 From: salmonik Date: Sun, 12 Jul 2026 19:38:03 +0200 Subject: first stable --- src/rcon.rs | 133 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 133 insertions(+) create mode 100644 src/rcon.rs (limited to 'src/rcon.rs') diff --git a/src/rcon.rs b/src/rcon.rs new file mode 100644 index 0000000..b91fe9b --- /dev/null +++ b/src/rcon.rs @@ -0,0 +1,133 @@ +use std::io::{Read, Write}; +use std::net::TcpStream; + +#[derive(Debug, Clone)] +pub struct RconClient { + host: String, + port: u16, + password: String, +} + +#[derive(Debug)] +pub enum RconError { + Connection(String), + Auth(String), + Response(String), +} + +impl std::fmt::Display for RconError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + RconError::Connection(e) => write!(f, "RCON connection error: {e}"), + RconError::Auth(e) => write!(f, "RCON auth error: {e}"), + RconError::Response(e) => write!(f, "RCON response error: {e}"), + } + } +} + +const SERVERDATA_AUTH: i32 = 3; +const SERVERDATA_EXECCOMMAND: i32 = 2; + +impl RconClient { + pub fn new(password: String) -> Self { + Self { + host: "127.0.0.1".to_string(), + port: 25575, + password, + } + } + + /// Non-blocking async wrapper — runs the blocking RCON call on a thread pool. + pub async fn send_command_async(&self, command: String) -> Result { + let client = self.clone(); + tokio::task::spawn_blocking(move || client.send_command(&command)) + .await + .map_err(|e| RconError::Connection(e.to_string()))? + } + + fn send_command(&self, command: &str) -> Result { + let mut stream = TcpStream::connect(format!("{}:{}", self.host, self.port)) + .map_err(|e| RconError::Connection(e.to_string()))?; + + stream + .set_read_timeout(Some(std::time::Duration::from_secs(1))) + .ok(); + + let request_id = 1; + self.write_packet(&mut stream, request_id, SERVERDATA_AUTH, &self.password)?; + let response = self.read_packet(&mut stream)?; + + if response.request_id == -1 { + return Err(RconError::Auth("Invalid RCON password".into())); + } + + let cmd_id = 2; + self.write_packet(&mut stream, cmd_id, SERVERDATA_EXECCOMMAND, command)?; + + let response = self.read_packet(&mut stream)?; + // Second packet is an end marker — some servers send it, some don't; ignore errors + let _ = self.read_packet(&mut stream); + + Ok(response.body) + } + + fn write_packet( + &self, + stream: &mut TcpStream, + request_id: i32, + packet_type: i32, + body: &str, + ) -> Result<(), RconError> { + let body_bytes = body.as_bytes(); + let length = 10 + body_bytes.len() as i32; + + let mut packet = Vec::with_capacity(4 + length as usize); + packet.extend_from_slice(&length.to_le_bytes()); + packet.extend_from_slice(&request_id.to_le_bytes()); + packet.extend_from_slice(&packet_type.to_le_bytes()); + packet.extend_from_slice(body_bytes); + packet.extend_from_slice(&[0u8, 0u8]); + + stream + .write_all(&packet) + .map_err(|e| RconError::Connection(e.to_string()))?; + + Ok(()) + } + + fn read_packet(&self, stream: &mut TcpStream) -> Result { + let mut length_buf = [0u8; 4]; + stream + .read_exact(&mut length_buf) + .map_err(|e| RconError::Response(format!("Failed to read length: {e}")))?; + + let length = i32::from_le_bytes(length_buf); + if length < 10 { + return Err(RconError::Response("Packet too short".into())); + } + + let mut payload = vec![0u8; length as usize]; + stream + .read_exact(&mut payload) + .map_err(|e| RconError::Response(format!("Failed to read payload: {e}")))?; + + let request_id = i32::from_le_bytes([payload[0], payload[1], payload[2], payload[3]]); + let packet_type = i32::from_le_bytes([payload[4], payload[5], payload[6], payload[7]]); + + let body_bytes = &payload[8..payload.len() - 2]; + let body = String::from_utf8_lossy(body_bytes).to_string(); + + Ok(RconPacket { + request_id, + packet_type, + body, + }) + } +} + +#[derive(Debug)] +struct RconPacket { + request_id: i32, + packet_type: i32, + body: String, +} -- cgit v1.2.3