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 body_bytes = &payload[8..payload.len() - 2]; let body = String::from_utf8_lossy(body_bytes).to_string(); Ok(RconPacket { request_id, body }) } } #[derive(Debug)] struct RconPacket { request_id: i32, body: String, }