Skip to main content

mc_minder/rcon/
mod.rs

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}