mc_rcon/
lib.rs

1#![cfg_attr(docs_rs, feature(doc_auto_cfg))]
2#![warn(missing_docs)]
3
4//! This crate provides a client for Minecraft's RCON protocol as specified at <https://wiki.vg/RCON>.
5//! 
6//! Connect to a server with [`RconClient::connect`], log in with [`RconClient::log_in`], and then send your commands [`RconClient::send_command`].
7//! For example:
8//! 
9//! ```no_run
10//! # use std::error::Error;
11//! # 
12//! # use mc_rcon::RconClient;
13//! # 
14//! # fn main() -> Result<(), Box<dyn Error>> {
15//! let client = RconClient::connect("localhost:25575")?;
16//! client.log_in("SuperSecurePassword")?;
17//! println!("{}", client.send_command("seed")?);
18//! #   Ok(())
19//! # }
20//! ```
21//! 
22//! This example connects to a server running on localhost,
23//! with RCON configured on port 25575 (or omitted, as that is the default port)
24//! and with password `SuperSecurePassword`,
25//! after which it uses Minecraft's `seed` command to query the world's generation seed.
26//! 
27//! Assuming that the server is configured accordingly, this program will print a response from the server like `Seed: [-1137927873379713691]`.
28//! 
29//! Note that, although RCON servers [can send multiple response packets](https://wiki.vg/RCON#Fragmentation), this crate currently does not handle that possibility.
30//! If you need that functionality, please open an issue.
31
32use std::{error::Error, fmt::{self, Debug, Display, Formatter}, io::{self, Read, Write}, mem::size_of, net::{TcpStream, ToSocketAddrs}, sync::atomic::{AtomicBool, AtomicI32, Ordering::SeqCst}};
33
34use arrayvec::ArrayVec;
35
36/// The default port used by Minecraft for RCON.
37/// 
38/// This crate does not use this value, it is simply here for convenience and completeness.
39pub const DEFAULT_RCON_PORT: u16 = 25575;
40
41/// The maximum number of payload bytes that an RCON server will accept.
42/// 
43/// If users of this crate try to send passwords or commands longer than this,
44/// they will get a [`LogInError::PasswordTooLong`] or a [`CommandError::CommandTooLong`],
45/// and nothing will be sent to the server.
46pub const MAX_OUTGOING_PAYLOAD_LEN: usize = 1446; // does not include nul terminator
47
48/// The maximum number of payload bytes that an RCON server will send in one packet.
49/// 
50/// Currently, users of this crate can expect command responses to have lengths less that or equal to this value,
51/// though that may change in the future given that servers may send multiple response packets.
52pub const MAX_INCOMING_PAYLOAD_LEN: usize = 4096; // does not include nul terminator
53
54const HEADER_LEN: usize = 10;
55
56const LOGIN_TYPE: i32 = 3;
57
58const COMMAND_TYPE: i32 = 2;
59
60/// A client that has connected to an RCON server.
61/// 
62/// See the [crate-level documentation](crate) for an example.
63#[derive(Debug)]
64pub struct RconClient {
65  
66  stream: TcpStream,
67  next_id: AtomicI32,
68  logged_in: AtomicBool
69  
70}
71
72impl RconClient {
73  
74  /// Construct a `RconClient` and connect to a server at the given address.
75  /// 
76  /// See the [crate-level documentation](crate) for an example.
77  /// 
78  /// # Errors
79  /// 
80  /// This function errors if any I/O errors occur while setting up the connection.
81  /// Most notably, if the server is not running or RCON is not enabled,
82  /// this method will error with [`ConnectionRefused`](std::io::ErrorKind::ConnectionRefused).
83  pub fn connect<A: ToSocketAddrs>(server_addr: A) -> io::Result<RconClient> {
84    let stream = TcpStream::connect(server_addr)?;
85    stream.set_nonblocking(false)?;
86    stream.set_read_timeout(None)?;
87    Ok(RconClient { stream, next_id: AtomicI32::new(0), logged_in: AtomicBool::new(false) })
88  }
89  
90  /// Returns whether this client is logged in.
91  /// 
92  /// Example:
93  /// ```no_run
94  /// # use std::error::Error;
95  /// # use mc_rcon::RconClient;
96  /// # 
97  /// # fn main() -> Result<(), Box<dyn Error>> {
98  /// let client = RconClient::connect("localhost:25575")?;
99  /// assert!(!client.is_logged_in());
100  /// client.log_in("SuperSecurePassword")?;
101  /// assert!(client.is_logged_in());
102  /// #   Ok(())
103  /// # }
104  /// ```
105  pub fn is_logged_in(&self) -> bool {
106    self.logged_in.load(SeqCst)
107  }
108  
109  fn send_log_in(&self, password: &str) -> Result<(), LogInError> {
110    if self.is_logged_in() {
111      Err(LogInError::AlreadyLoggedIn)?
112    }
113    let SendResponse { good_auth, payload: _ } = self.send(LogInPacket, password)?;
114    if good_auth {
115      Ok(())
116    } else {
117      Err(LogInError::BadPassword)
118    }
119  }
120  
121  fn get_next_id(&self) -> i32 {
122    let mut id = self.next_id.fetch_add(1, SeqCst);
123    if id == -1 { // skip id -1 so that authentication failures can always be identified
124      id = self.next_id.fetch_add(1, SeqCst)
125    }
126    id
127  }
128  
129  fn send<K: PacketKind>(&self, kind: K, payload: &str) -> Result<SendResponse, SendError> {
130    let _ = kind;
131    if payload.len() > MAX_OUTGOING_PAYLOAD_LEN {
132      Err(SendError::PayloadTooLong)?
133    }
134    
135    const I32_LEN: usize = size_of::<i32>();
136    
137    let out_len = i32::try_from(HEADER_LEN + payload.len()).expect("payload is too long");
138    let out_id = self.get_next_id();
139    
140    let mut stream = &self.stream;
141    // Buffering this apparently helps prevent MC from reading a packet of length < 10 and consequently disconnecting
142    // I could use BufWriter, but in this case I know the exact max size, so this is probably cheaper (and I just like ArrayVec, and consequently take every opportunity to use it)
143    let mut out_buf: ArrayVec<u8, {I32_LEN + HEADER_LEN + MAX_OUTGOING_PAYLOAD_LEN}> = ArrayVec::new();
144    out_buf.write_all(&out_len.to_le_bytes())?;
145    out_buf.write_all(&out_id.to_le_bytes())?;
146    out_buf.write_all(&K::TYPE.to_le_bytes())?;
147    out_buf.write_all(payload.as_bytes())?;
148    out_buf.write_all(b"\0\0")?; // null terminator and padding
149    debug_assert_eq!(out_buf.len(), I32_LEN + HEADER_LEN + payload.len());
150    stream.write_all(&mut out_buf)?;
151    stream.flush()?;
152    
153    let mut in_len_bytes = [0; I32_LEN];
154    let mut in_id_bytes = [0; I32_LEN];
155    stream.read_exact(&mut in_len_bytes)?;
156    let in_len = i32::from_le_bytes(in_len_bytes);
157    stream.read_exact(&mut in_id_bytes)?;
158    let in_id = i32::from_le_bytes(in_id_bytes);
159    stream.read_exact(&mut [0; I32_LEN])?;
160    let payload_len = usize::try_from(in_len).expect("payload is too long") - HEADER_LEN;
161    let mut payload_buf = vec![0; payload_len];
162    stream.read_exact(&mut payload_buf)?;
163    stream.read_exact(&mut [0; 2])?; // expect null terminator and padding
164      
165    let good_auth = if in_id == -1 {
166      false
167    } else if in_id == out_id {
168      true
169    } else {
170      Err(io::Error::new(io::ErrorKind::InvalidData, K::INVLID_RESPONSE_ID_ERROR))?
171    };
172    
173    if K::ACCEPTS_LONG_RESPONSES && payload_len >= MAX_INCOMING_PAYLOAD_LEN {
174      const CAP_COMMAND: &'static str = "seed";
175      let cap_len = i32::try_from(HEADER_LEN + CAP_COMMAND.len()).expect("cap payload is somehow too long");
176      let cap_id = self.get_next_id();
177      let mut cap_buf: ArrayVec<u8, {I32_LEN + HEADER_LEN + CAP_COMMAND.len()}> = ArrayVec::new();
178      cap_buf.write_all(&cap_len.to_le_bytes())?;
179      cap_buf.write_all(&cap_id.to_le_bytes())?;
180      cap_buf.write_all(&K::TYPE.to_le_bytes())?;
181      cap_buf.write_all(CAP_COMMAND.as_bytes())?;
182      cap_buf.write_all(b"\0\0")?;
183      debug_assert_eq!(cap_buf.len(), I32_LEN + HEADER_LEN + CAP_COMMAND.len());
184      stream.write_all(&mut cap_buf)?;
185      stream.flush()?;
186      
187      loop {
188        stream.read_exact(&mut in_len_bytes)?;
189        let inner_in_len = i32::from_le_bytes(in_len_bytes);
190        stream.read_exact(&mut in_id_bytes)?;
191        let inner_in_id = i32::from_le_bytes(in_id_bytes);
192        stream.read_exact(&mut [0; I32_LEN])?;
193        let inner_payload_len = usize::try_from(inner_in_len).expect("payload is too long") - HEADER_LEN;
194        let mut inner_payload_buf = vec![0; inner_payload_len];
195        stream.read_exact(&mut inner_payload_buf)?;
196        stream.read_exact(&mut [0; 2])?;
197        
198        if inner_in_id == cap_id {
199          break
200        } else if inner_in_id == in_id {
201          payload_buf.append(&mut inner_payload_buf);
202        } else if inner_in_id == -1 {
203          Err(io::Error::new(io::ErrorKind::InvalidData, "client became deauthenticated between packets"))?
204        } else {
205          Err(io::Error::new(io::ErrorKind::InvalidData, K::INVLID_RESPONSE_ID_ERROR))?
206        }
207      }
208    }
209    
210    let payload = String::from_utf8(payload_buf).expect("response payload is not ASCII");
211    Ok(SendResponse { good_auth, payload })
212  }
213  
214  /// Attempts to log into the server with the given password.
215  /// 
216  /// See the [crate-level documentation](crate) for an example.
217  /// 
218  /// # Errors
219  /// 
220  /// * If the password is longer than [`MAX_OUTGOING_PAYLOAD_LEN`], returns [`LogInError::PasswordTooLong`] and does not send anything to the server.
221  /// * If this client is already logged in, returns [`LogInError::AlreadyLoggedIn`] and does not send anything to the server.
222  /// * If the given password is successfully sent, and the server responds indicating failure, returns [`LogInError::BadPassword`].
223  /// * If any I/O errors occur, returns [`LogInError::IO`] with the error.
224  ///   This notably includes [`ConnectionAborted`](std::io::ErrorKind::ConnectionAborted) if the server has closed the connection.
225  pub fn log_in(&self, password: &str) -> Result<(), LogInError> {
226    self.send_log_in(password)?;
227    self.logged_in.store(true, SeqCst);
228    Ok(())
229  }
230  
231  /// Sends the given command to the server and returns its response.
232  /// 
233  /// See the [crate-level documentation](crate) for an example.
234  /// 
235  /// Valid commands are dependent on the server;
236  /// documentation on the commands offered by vanilla Minecraft can be found at <https://minecraft.wiki/w/Commands>.
237  /// Servers with mods or plugins may have other commands available.
238  /// 
239  /// The return value of this method is the response message from the server.
240  /// This crate makes no attempt to interpret that response;
241  /// in particular, this method will never error to indicate that the command failed:
242  /// a successful return only means that the server recieved this command and responded to it.
243  /// 
244  /// # Errors
245  /// 
246  /// * If the command is longer than [`MAX_OUTGOING_PAYLOAD_LEN`], returns [`CommandError::CommandTooLong`] and does not send anything to the server.
247  /// * If this client is not logged in, returns [`CommandError::NotLoggedIn`] and does not send anything to the server.
248  /// * If any I/O errors occur, returns [`CommandError::IO`] with the error.
249  ///   This notably includes [`ConnectionAborted`](std::io::ErrorKind::ConnectionAborted) if the server has closed the connection.
250  pub fn send_command(&self, command: &str) -> Result<String, CommandError> {
251    if !self.is_logged_in() {
252      Err(CommandError::NotLoggedIn)?
253    }
254    let SendResponse { good_auth, payload } = self.send(CommandPacket, command)?;
255    if good_auth {
256      Ok(payload)
257    } else {
258      Err(CommandError::NotLoggedIn)
259    }
260  }
261  
262}
263
264trait PacketKind {
265  
266  const ACCEPTS_LONG_RESPONSES: bool;
267  
268  const TYPE: i32;
269  
270  const INVLID_RESPONSE_ID_ERROR: &'static str;
271  
272}
273
274struct LogInPacket;
275
276impl PacketKind for LogInPacket {
277  
278  const ACCEPTS_LONG_RESPONSES: bool = false;
279  
280  const TYPE: i32 = LOGIN_TYPE;
281  
282  const INVLID_RESPONSE_ID_ERROR: &'static str = "response packet id mismatched with login packet id";
283  
284}
285
286struct CommandPacket;
287
288impl PacketKind for CommandPacket {
289  
290  const ACCEPTS_LONG_RESPONSES: bool = true;
291  
292  const TYPE: i32 = COMMAND_TYPE;
293  
294  const INVLID_RESPONSE_ID_ERROR: &'static str = "response packet id mismatched with command packet id";
295  
296}
297
298#[derive(Debug)]
299struct SendResponse {
300  
301  good_auth: bool,
302  payload: String
303  
304}
305
306/// A failed attempt to log in. See [`RconClient::log_in`] for details.
307#[derive(Debug)]
308pub enum LogInError {
309  
310  /// An I/O error occured.
311  IO(io::Error),
312  /// The password was too long.
313  PasswordTooLong,
314  /// The client is already logged in.
315  AlreadyLoggedIn,
316  /// The password was incorrect.
317  BadPassword
318  
319}
320
321impl From<io::Error> for LogInError {
322  
323  fn from(e: io::Error) -> Self {
324    LogInError::IO(e)
325  }
326  
327}
328
329impl From<SendError> for LogInError {
330  
331  fn from(e: SendError) -> Self {
332    match e {
333      SendError::IO(e) => LogInError::IO(e),
334      SendError::PayloadTooLong => LogInError::PasswordTooLong
335    }
336  }
337  
338}
339
340impl Display for LogInError {
341  
342  fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
343    match self {
344      LogInError::IO(e) => Display::fmt(e, f),
345      LogInError::PasswordTooLong => write!(f, "password must be no longer than {} bytes", MAX_OUTGOING_PAYLOAD_LEN),
346      LogInError::AlreadyLoggedIn => write!(f, "tried to log in when already logged in"),
347      LogInError::BadPassword => write!(f, "tried to log in with incorrect password")
348    }
349  }
350  
351}
352
353impl Error for LogInError {}
354
355/// A failed attempt to send a command. See [`RconClient::send_command`] for details.
356#[derive(Debug)]
357pub enum CommandError {
358  
359  /// An I/O error occurred.
360  IO(io::Error),
361  /// The command was too long.
362  CommandTooLong,
363  /// The client is not logged in.
364  NotLoggedIn
365  
366}
367
368impl From<io::Error> for CommandError {
369  
370  fn from(e: io::Error) -> Self {
371    CommandError::IO(e)
372  }
373  
374}
375
376impl From<SendError> for CommandError {
377  
378  fn from(e: SendError) -> Self {
379    match e {
380      SendError::IO(e) => CommandError::IO(e),
381      SendError::PayloadTooLong => CommandError::CommandTooLong
382    }
383  }
384  
385}
386
387impl Display for CommandError {
388  
389  fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
390    match self {
391      CommandError::IO(e) => Display::fmt(e, f),
392      CommandError::CommandTooLong => write!(f, "command must be no longer than {} bytes", MAX_OUTGOING_PAYLOAD_LEN),
393      CommandError::NotLoggedIn => write!(f, "tried to send a command before logging in")
394    }
395  }
396  
397}
398
399impl Error for CommandError {}
400
401#[derive(Debug)]
402enum SendError {
403  
404  IO(io::Error),
405  PayloadTooLong
406  
407}
408
409impl From<io::Error> for SendError {
410  
411  fn from(e: io::Error) -> Self {
412    SendError::IO(e)
413  }
414  
415}