summaryrefslogtreecommitdiffstats
path: root/src/rcon.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/rcon.rs')
-rw-r--r--src/rcon.rs133
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 @@
1use std::io::{Read, Write};
2use std::net::TcpStream;
3
4#[derive(Debug, Clone)]
5pub struct RconClient {
6 host: String,
7 port: u16,
8 password: String,
9}
10
11#[derive(Debug)]
12pub enum RconError {
13 Connection(String),
14 Auth(String),
15 Response(String),
16}
17
18impl 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
28const SERVERDATA_AUTH: i32 = 3;
29const SERVERDATA_EXECCOMMAND: i32 = 2;
30
31impl 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)]
129struct RconPacket {
130 request_id: i32,
131 packet_type: i32,
132 body: String,
133}