diff options
| author | salmonik <salmonik@tuta.io> | 2026-07-12 19:38:03 +0200 |
|---|---|---|
| committer | salmonik <salmonik@tuta.io> | 2026-07-12 19:38:03 +0200 |
| commit | ae7ef10b67737f08a86743f687bca30a12e9e17a (patch) | |
| tree | 056539ab76b801559dc8d07ee6599220e2bd8e07 /src/rcon.rs | |
| parent | architecture (diff) | |
| download | minecraft-ui-ae7ef10b67737f08a86743f687bca30a12e9e17a.tar.gz minecraft-ui-ae7ef10b67737f08a86743f687bca30a12e9e17a.zip | |
first stable
Diffstat (limited to '')
| -rw-r--r-- | src/rcon.rs | 133 |
1 files changed, 133 insertions, 0 deletions
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 @@ | |||
| 1 | use std::io::{Read, Write}; | ||
| 2 | use std::net::TcpStream; | ||
| 3 | |||
| 4 | #[derive(Debug, Clone)] | ||
| 5 | pub struct RconClient { | ||
| 6 | host: String, | ||
| 7 | port: u16, | ||
| 8 | password: String, | ||
| 9 | } | ||
| 10 | |||
| 11 | #[derive(Debug)] | ||
| 12 | pub enum RconError { | ||
| 13 | Connection(String), | ||
| 14 | Auth(String), | ||
| 15 | Response(String), | ||
| 16 | } | ||
| 17 | |||
| 18 | impl std::fmt::Display for RconError { | ||
| 19 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | ||
| 20 | match self { | ||
| 21 | RconError::Connection(e) => write!(f, "RCON connection error: {e}"), | ||
| 22 | RconError::Auth(e) => write!(f, "RCON auth error: {e}"), | ||
| 23 | RconError::Response(e) => write!(f, "RCON response error: {e}"), | ||
| 24 | } | ||
| 25 | } | ||
| 26 | } | ||
| 27 | |||
| 28 | const SERVERDATA_AUTH: i32 = 3; | ||
| 29 | const SERVERDATA_EXECCOMMAND: i32 = 2; | ||
| 30 | |||
| 31 | impl RconClient { | ||
| 32 | pub fn new(password: String) -> Self { | ||
| 33 | Self { | ||
| 34 | host: "127.0.0.1".to_string(), | ||
| 35 | port: 25575, | ||
| 36 | password, | ||
| 37 | } | ||
| 38 | } | ||
| 39 | |||
| 40 | /// Non-blocking async wrapper — runs the blocking RCON call on a thread pool. | ||
| 41 | pub async fn send_command_async(&self, command: String) -> Result<String, RconError> { | ||
| 42 | let client = self.clone(); | ||
| 43 | tokio::task::spawn_blocking(move || client.send_command(&command)) | ||
| 44 | .await | ||
| 45 | .map_err(|e| RconError::Connection(e.to_string()))? | ||
| 46 | } | ||
| 47 | |||
| 48 | fn send_command(&self, command: &str) -> Result<String, RconError> { | ||
| 49 | let mut stream = TcpStream::connect(format!("{}:{}", self.host, self.port)) | ||
| 50 | .map_err(|e| RconError::Connection(e.to_string()))?; | ||
| 51 | |||
| 52 | stream | ||
| 53 | .set_read_timeout(Some(std::time::Duration::from_secs(1))) | ||
| 54 | .ok(); | ||
| 55 | |||
| 56 | let request_id = 1; | ||
| 57 | self.write_packet(&mut stream, request_id, SERVERDATA_AUTH, &self.password)?; | ||
| 58 | let response = self.read_packet(&mut stream)?; | ||
| 59 | |||
| 60 | if response.request_id == -1 { | ||
| 61 | return Err(RconError::Auth("Invalid RCON password".into())); | ||
| 62 | } | ||
| 63 | |||
| 64 | let cmd_id = 2; | ||
| 65 | self.write_packet(&mut stream, cmd_id, SERVERDATA_EXECCOMMAND, command)?; | ||
| 66 | |||
| 67 | let response = self.read_packet(&mut stream)?; | ||
| 68 | // Second packet is an end marker — some servers send it, some don't; ignore errors | ||
| 69 | let _ = self.read_packet(&mut stream); | ||
| 70 | |||
| 71 | Ok(response.body) | ||
| 72 | } | ||
| 73 | |||
| 74 | fn write_packet( | ||
| 75 | &self, | ||
| 76 | stream: &mut TcpStream, | ||
| 77 | request_id: i32, | ||
| 78 | packet_type: i32, | ||
| 79 | body: &str, | ||
| 80 | ) -> Result<(), RconError> { | ||
| 81 | let body_bytes = body.as_bytes(); | ||
| 82 | let length = 10 + body_bytes.len() as i32; | ||
| 83 | |||
| 84 | let mut packet = Vec::with_capacity(4 + length as usize); | ||
| 85 | packet.extend_from_slice(&length.to_le_bytes()); | ||
| 86 | packet.extend_from_slice(&request_id.to_le_bytes()); | ||
| 87 | packet.extend_from_slice(&packet_type.to_le_bytes()); | ||
| 88 | packet.extend_from_slice(body_bytes); | ||
| 89 | packet.extend_from_slice(&[0u8, 0u8]); | ||
| 90 | |||
| 91 | stream | ||
| 92 | .write_all(&packet) | ||
| 93 | .map_err(|e| RconError::Connection(e.to_string()))?; | ||
| 94 | |||
| 95 | Ok(()) | ||
| 96 | } | ||
| 97 | |||
| 98 | fn read_packet(&self, stream: &mut TcpStream) -> Result<RconPacket, RconError> { | ||
| 99 | let mut length_buf = [0u8; 4]; | ||
| 100 | stream | ||
| 101 | .read_exact(&mut length_buf) | ||
| 102 | .map_err(|e| RconError::Response(format!("Failed to read length: {e}")))?; | ||
| 103 | |||
| 104 | let length = i32::from_le_bytes(length_buf); | ||
| 105 | if length < 10 { | ||
| 106 | return Err(RconError::Response("Packet too short".into())); | ||
| 107 | } | ||
| 108 | |||
| 109 | let mut payload = vec![0u8; length as usize]; | ||
| 110 | stream | ||
| 111 | .read_exact(&mut payload) | ||
| 112 | .map_err(|e| RconError::Response(format!("Failed to read payload: {e}")))?; | ||
| 113 | |||
| 114 | let request_id = i32::from_le_bytes([payload[0], payload[1], payload[2], payload[3]]); | ||
| 115 | let packet_type = i32::from_le_bytes([payload[4], payload[5], payload[6], payload[7]]); | ||
| 116 | |||
| 117 | let body_bytes = &payload[8..payload.len() - 2]; | ||
| 118 | let body = String::from_utf8_lossy(body_bytes).to_string(); | ||
| 119 | |||
| 120 | Ok(RconPacket { | ||
| 121 | request_id, | ||
| 122 | packet_type, | ||
| 123 | body, | ||
| 124 | }) | ||
| 125 | } | ||
| 126 | } | ||
| 127 | |||
| 128 | #[derive(Debug)] | ||
| 129 | struct RconPacket { | ||
| 130 | request_id: i32, | ||
| 131 | packet_type: i32, | ||
| 132 | body: String, | ||
| 133 | } | ||
