mc_query/rcon/client.rs
1//! Implementation of the [RCON](https://wiki.vg/RCON) protocol.
2
3use super::{
4 packet::{RconPacket, RconPacketType},
5 MAX_LEN_CLIENTBOUND,
6};
7use crate::errors::{timeout_err, RconProtocolError};
8use bytes::{BufMut, BytesMut};
9use std::time::Duration;
10use tokio::{
11 io::{self, AsyncReadExt, AsyncWriteExt, Error},
12 net::TcpStream,
13 time::timeout,
14};
15
16/// Struct that stores the connection and other state of the RCON protocol with the server.
17///
18/// # Examples
19///
20/// ```no_run
21/// use mc_query::rcon::RconClient;
22/// use tokio::io::Result;
23///
24/// #[tokio::main]
25/// async fn main() -> Result<()> {
26/// let mut client = RconClient::new("localhost", 25575).await?;
27/// client.authenticate("password").await?;
28///
29/// let output = client.run_command("time set day").await?;
30/// println!("{output}");
31///
32/// Ok(())
33/// }
34/// ```
35#[allow(clippy::module_name_repetitions)]
36#[derive(Debug)]
37pub struct RconClient {
38 socket: TcpStream,
39 timeout: Option<Duration>,
40}
41
42impl RconClient {
43 /// Construct an [`RconClient`] that connects to the given host and port.
44 /// Note: to authenticate use the `authenticate` method, this method does not take a password.
45 ///
46 /// Clients constructed this way will wait arbitrarily long (maybe forever!) to recieve
47 /// a response from the server. To set a timeout, see [`with_timeout`] or [`set_timeout`].
48 ///
49 /// # Arguments
50 /// * `host` - A string slice that holds the hostname of the server to connect to.
51 /// * `port` - The port to connect to.
52 ///
53 /// # Errors
54 /// Returns `Err` if there was a network error.
55 pub async fn new(host: &str, port: u16) -> io::Result<Self> {
56 let connection = TcpStream::connect(format!("{host}:{port}")).await?;
57
58 Ok(Self {
59 socket: connection,
60 timeout: None,
61 })
62 }
63
64 /// Construct an [`RconClient`] that connects to the given host and port, and a connection
65 /// timeout.
66 /// Note: to authenticate use the `authenticate` method, this method does not take a password.
67 ///
68 /// Note that timeouts are not precise, and may vary on the order of milliseconds, because
69 /// of the way the async event loop works.
70 ///
71 /// # Arguments
72 /// * `host` - A string slice that holds the hostname of the server to connect to.
73 /// * `port` - The port to connect to.
74 /// * `timeout` - A duration to wait for each response to arrive in.
75 ///
76 /// # Errors
77 /// Returns `Err` if there was a network error.
78 pub async fn with_timeout(host: &str, port: u16, timeout: Duration) -> io::Result<Self> {
79 let mut client = Self::new(host, port).await?;
80 client.set_timeout(Some(timeout));
81
82 Ok(client)
83 }
84
85 /// Change the timeout for future requests.
86 ///
87 /// # Arguments
88 /// * `timeout` - an option specifying the duration to wait for a response.
89 /// if none, the client may wait forever.
90 pub fn set_timeout(&mut self, timeout: Option<Duration>) {
91 self.timeout = timeout;
92 }
93
94 /// Disconnect from the server and close the RCON connection.
95 ///
96 /// # Errors
97 /// Returns `Err` if there was an issue closing the connection.
98 pub async fn disconnect(mut self) -> io::Result<()> {
99 self.socket.shutdown().await
100 }
101
102 /// Authenticate with the server, with the given password.
103 ///
104 /// If authentication fails, this method will return [`RconProtocolError::AuthFailed`].
105 ///
106 /// # Arguments
107 /// * `password` - A string slice that holds the RCON password.
108 ///
109 /// # Errors
110 /// Returns the raw `tokio::io::Error` if there was a network error.
111 /// Returns an apprpriate [`RconProtocolError`] if the authentication failed for other reasons.
112 /// Also returns an error if a timeout is set, and the response is not recieved in that timeframe.
113 pub async fn authenticate(&mut self, password: &str) -> io::Result<()> {
114 let to = self.timeout;
115 let fut = self.authenticate_raw(password);
116
117 match to {
118 None => fut.await,
119 Some(d) => timeout(d, fut).await.unwrap_or(timeout_err()),
120 }
121 }
122
123 /// Run the given command on the server and return the result.
124 ///
125 /// # Arguments
126 /// * `command` - A string slice that holds the command to run. Must be ASCII and under 1446 bytes in length.
127 ///
128 /// # Errors
129 /// Returns an error if there was a network issue or an [`RconProtocolError`] for other failures.
130 /// Also returns an error if a timeout was set and a response was not recieved in that timeframe.
131 pub async fn run_command(&mut self, command: &str) -> io::Result<String> {
132 let to = self.timeout;
133 let fut = self.run_command_raw(command);
134
135 match to {
136 None => fut.await,
137 Some(d) => timeout(d, fut).await.unwrap_or(timeout_err()),
138 }
139 }
140
141 async fn authenticate_raw(&mut self, password: &str) -> io::Result<()> {
142 let packet =
143 RconPacket::new(1, RconPacketType::Login, password.to_string()).map_err(Error::from)?;
144
145 self.write_packet(packet).await?;
146
147 let packet = self.read_packet().await?;
148
149 if !matches!(packet.packet_type, RconPacketType::RunCommand) {
150 return Err(RconProtocolError::InvalidPacketType.into());
151 }
152
153 if packet.request_id == -1 {
154 return Err(RconProtocolError::AuthFailed.into());
155 } else if packet.request_id != 1 {
156 return Err(RconProtocolError::RequestIdMismatch.into());
157 }
158
159 Ok(())
160 }
161
162 async fn run_command_raw(&mut self, command: &str) -> io::Result<String> {
163 let packet = RconPacket::new(1, RconPacketType::RunCommand, command.to_string())
164 .map_err(Error::from)?;
165
166 self.write_packet(packet).await?;
167
168 let mut full_payload = String::new();
169
170 loop {
171 let recieved = self.read_packet().await?;
172
173 if recieved.request_id == -1 {
174 return Err(RconProtocolError::AuthFailed.into());
175 } else if recieved.request_id != 1 {
176 return Err(RconProtocolError::RequestIdMismatch.into());
177 }
178
179 full_payload.push_str(&recieved.payload);
180
181 // wiki says this method of determining if this is the end of the
182 // response is not 100% reliable, but this is the best solution imo
183 // if this ends up being a problem, this can be changed later
184 if recieved.payload.len() < MAX_LEN_CLIENTBOUND {
185 break;
186 }
187 }
188
189 Ok(full_payload)
190 }
191
192 /// Read a packet from the socket.
193 async fn read_packet(&mut self) -> io::Result<RconPacket> {
194 let len = self.socket.read_i32_le().await?;
195
196 let mut bytes = BytesMut::new();
197 bytes.put_i32_le(len);
198
199 for _ in 0..len {
200 let current = self.socket.read_u8().await?;
201 bytes.put_u8(current);
202 }
203
204 RconPacket::try_from(bytes.freeze()).map_err(Error::from)
205 }
206
207 /// Write a packet to the socket.
208 ///
209 /// # Arguments
210 /// * `packet` - An owned [`RconPacket`] to write to the socket.
211 async fn write_packet(&mut self, packet: RconPacket) -> io::Result<()> {
212 let bytes = packet.bytes();
213
214 self.socket.write_all(&bytes).await
215 }
216}
217
218#[cfg(test)]
219mod tests {
220 use super::RconClient;
221 use tokio::io;
222
223 #[tokio::test]
224 async fn test_rcon_command() -> io::Result<()> {
225 let mut client = RconClient::new("localhost", 25575).await?;
226 client.authenticate("mc-query-test").await?;
227 let response = client.run_command("time set day").await?;
228
229 println!("recieved response: {response}");
230
231 Ok(())
232 }
233
234 #[tokio::test]
235 async fn test_rcon_unauthenticated() -> io::Result<()> {
236 let mut client = RconClient::new("localhost", 25575).await?;
237 let result = client.run_command("time set day").await;
238
239 assert!(result.is_err());
240
241 Ok(())
242 }
243
244 #[tokio::test]
245 async fn test_rcon_incorrect_password() -> io::Result<()> {
246 let mut client = RconClient::new("localhost", 25575).await?;
247 let result = client.authenticate("incorrect").await;
248
249 assert!(result.is_err());
250
251 Ok(())
252 }
253}