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}