1use anyhow::{Result, Context};
2use log::{info, debug};
3use std::io::{Read, Write};
4use std::net::TcpStream;
5use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt};
6use std::time::Duration;
7
8pub struct RconClient {
9 host: String,
10 port: u16,
11 password: String,
12 stream: Option<TcpStream>,
13}
14
15const PACKET_TYPE_AUTH: i32 = 3;
16const PACKET_TYPE_EXEC_COMMAND: i32 = 2;
17
18struct Packet {
19 id: i32,
20 packet_type: i32,
21 payload: String,
22}
23
24impl RconClient {
25 pub fn new(host: String, port: u16, password: String) -> Self {
26 Self {
27 host,
28 port,
29 password,
30 stream: None,
31 }
32 }
33
34 pub fn connect(&mut self) -> Result<()> {
35 let addr = format!("{}:{}", self.host, self.port);
36 info!("Connecting to RCON at {}", addr);
37
38 let stream = TcpStream::connect_timeout(
39 &addr.parse().context("Invalid RCON address")?,
40 Duration::from_secs(5),
41 ).context("Failed to connect to RCON server")?;
42
43 stream.set_read_timeout(Some(Duration::from_secs(10)))?;
44 stream.set_write_timeout(Some(Duration::from_secs(10)))?;
45
46 self.stream = Some(stream);
47
48 self.authenticate()?;
49
50 info!("RCON authenticated successfully");
51 Ok(())
52 }
53
54 fn authenticate(&mut self) -> Result<()> {
55 let packet = Packet {
56 id: 1,
57 packet_type: PACKET_TYPE_AUTH,
58 payload: self.password.clone(),
59 };
60
61 self.send_packet(&packet)?;
62 let response = self.read_packet()?;
63
64 if response.id == -1 {
65 anyhow::bail!("RCON authentication failed");
66 }
67
68 Ok(())
69 }
70
71 pub fn execute(&mut self, command: &str) -> Result<String> {
72 if self.stream.is_none() {
73 self.connect()?;
74 }
75
76 let packet = Packet {
77 id: 1,
78 packet_type: PACKET_TYPE_EXEC_COMMAND,
79 payload: command.to_string(),
80 };
81
82 self.send_packet(&packet)?;
83 let response = self.read_packet()?;
84
85 Ok(response.payload)
86 }
87
88 fn send_packet(&mut self, packet: &Packet) -> Result<()> {
89 let stream = self.stream.as_mut().context("Not connected to RCON")?;
90
91 let payload_bytes = packet.payload.as_bytes();
92 let length = 10 + payload_bytes.len() as i32;
93
94 stream.write_i32::<BigEndian>(length)?;
95 stream.write_i32::<BigEndian>(packet.id)?;
96 stream.write_i32::<BigEndian>(packet.packet_type)?;
97 stream.write_all(payload_bytes)?;
98 stream.write_all(&[0, 0])?;
99 stream.flush()?;
100
101 debug!("Sent RCON packet: id={}, type={}", packet.id, packet.packet_type);
102 Ok(())
103 }
104
105 fn read_packet(&mut self) -> Result<Packet> {
106 let stream = self.stream.as_mut().context("Not connected to RCON")?;
107
108 let length = stream.read_i32::<BigEndian>()?;
109 let id = stream.read_i32::<BigEndian>()?;
110 let packet_type = stream.read_i32::<BigEndian>()?;
111
112 let payload_length = (length - 10) as usize;
113 if payload_length > 4096 {
114 anyhow::bail!("RCON packet too large");
115 }
116
117 let mut payload = vec![0u8; payload_length];
118 stream.read_exact(&mut payload)?;
119
120 let mut padding = [0u8; 2];
121 stream.read_exact(&mut padding)?;
122
123 let payload = String::from_utf8_lossy(&payload)
124 .trim_end_matches('\0')
125 .to_string();
126
127 debug!("Received RCON packet: id={}, type={}, payload_len={}", id, packet_type, payload.len());
128
129 Ok(Packet {
130 id,
131 packet_type,
132 payload,
133 })
134 }
135
136 pub fn disconnect(&mut self) {
137 if let Some(stream) = self.stream.take() {
138 let _ = stream.shutdown(std::net::Shutdown::Both);
139 info!("Disconnected from RCON");
140 }
141 }
142
143 pub fn say(&mut self, message: &str) -> Result<()> {
144 let command = format!("say {}", message);
145 self.execute(&command)?;
146 Ok(())
147 }
148
149 pub fn tell(&mut self, player: &str, message: &str) -> Result<()> {
150 let command = format!("tellraw {} {{\"text\":\"{}\"}}", player, message);
151 self.execute(&command)?;
152 Ok(())
153 }
154}
155
156impl Drop for RconClient {
157 fn drop(&mut self) {
158 self.disconnect();
159 }
160}