#![cfg_attr(docs_rs, feature(doc_auto_cfg))]
#![warn(missing_docs)]
use std::{error::Error, fmt::{self, Debug, Display, Formatter}, io::{self, Read, Write}, mem::size_of, net::{TcpStream, ToSocketAddrs}, sync::atomic::{AtomicBool, AtomicI32, Ordering::SeqCst}};
use arrayvec::ArrayVec;
pub const DEFAULT_RCON_PORT: u16 = 25575;
pub const MAX_OUTGOING_PAYLOAD_LEN: usize = 1446;
pub const MAX_INCOMING_PAYLOAD_LEN: usize = 4096;
const HEADER_LEN: usize = 10;
const LOGIN_TYPE: i32 = 3;
const COMMAND_TYPE: i32 = 2;
#[derive(Debug)]
pub struct RconClient {
stream: TcpStream,
next_id: AtomicI32,
logged_in: AtomicBool
}
impl RconClient {
pub fn connect<A: ToSocketAddrs>(server_addr: A) -> io::Result<RconClient> {
let stream = TcpStream::connect(server_addr)?;
stream.set_nonblocking(false)?;
stream.set_read_timeout(None)?;
Ok(RconClient { stream, next_id: AtomicI32::new(0), logged_in: AtomicBool::new(false) })
}
pub fn is_logged_in(&self) -> bool {
self.logged_in.load(SeqCst)
}
fn send_log_in(&self, password: &str) -> Result<(), LogInError> {
if self.is_logged_in() {
Err(LogInError::AlreadyLoggedIn)?
}
let SendResponse { good_auth, payload: _ } = self.send(LOGIN_TYPE, password)?;
if good_auth {
Ok(())
} else {
Err(LogInError::BadPassword)
}
}
fn get_next_id(&self) -> i32 {
let mut id = self.next_id.fetch_add(1, SeqCst);
if id == -1 { id = self.next_id.fetch_add(1, SeqCst)
}
id
}
fn send(&self, r#type: i32, payload: &str) -> Result<SendResponse, SendError> {
if payload.len() > MAX_OUTGOING_PAYLOAD_LEN {
Err(SendError::PayloadTooLong)?
}
const I32_LEN: usize = size_of::<i32>();
let out_len = i32::try_from(HEADER_LEN + payload.len()).expect("payload is too long");
let out_id = self.get_next_id();
let mut stream = &self.stream;
let mut out_buf: ArrayVec<u8, {I32_LEN + HEADER_LEN + MAX_OUTGOING_PAYLOAD_LEN}> = ArrayVec::new();
out_buf.write_all(&out_len.to_le_bytes())?;
out_buf.write_all(&out_id.to_le_bytes())?;
out_buf.write_all(&r#type.to_le_bytes())?;
out_buf.write_all(payload.as_bytes())?;
out_buf.write_all(&[b'\0', b'\0'])?; assert_eq!(out_buf.len(), I32_LEN + HEADER_LEN + payload.len());
stream.write_all(&mut out_buf)?;
stream.flush()?;
let mut in_len_bytes = [0; I32_LEN];
let mut in_id_bytes = [0; I32_LEN];
stream.read_exact(&mut in_len_bytes)?;
let in_len = i32::from_le_bytes(in_len_bytes);
stream.read_exact(&mut in_id_bytes)?;
let in_id = i32::from_le_bytes(in_id_bytes);
stream.read_exact(&mut [0; I32_LEN])?;
let payload_len = usize::try_from(in_len).expect("payload is too long") - HEADER_LEN;
let mut payload_buf = vec![0; payload_len];
stream.read_exact(&mut payload_buf)?;
stream.read_exact(&mut [0; 2])?; let payload = String::from_utf8(payload_buf).expect("response payload is not ASCII");
let good_auth = if in_id == -1 {
false
} else if in_id == out_id {
true
} else {
Err(io::Error::other("response packet id mismatched with login packet id"))?
};
Ok(SendResponse { good_auth, payload })
}
pub fn log_in(&self, password: &str) -> Result<(), LogInError> {
self.send_log_in(password)?;
self.logged_in.store(true, SeqCst);
Ok(())
}
pub fn send_command(&self, command: &str) -> Result<String, CommandError> {
if !self.is_logged_in() {
Err(CommandError::NotLoggedIn)?
}
let SendResponse { good_auth, payload } = self.send(COMMAND_TYPE, command)?;
if good_auth {
Ok(payload)
} else {
Err(CommandError::NotLoggedIn)
}
}
}
#[derive(Debug)]
struct SendResponse {
good_auth: bool,
payload: String
}
#[derive(Debug)]
pub enum LogInError {
IO(io::Error),
PasswordTooLong,
AlreadyLoggedIn,
BadPassword
}
impl From<io::Error> for LogInError {
fn from(e: io::Error) -> Self {
LogInError::IO(e)
}
}
impl From<SendError> for LogInError {
fn from(e: SendError) -> Self {
match e {
SendError::IO(e) => LogInError::IO(e),
SendError::PayloadTooLong => LogInError::PasswordTooLong
}
}
}
impl Display for LogInError {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match self {
LogInError::IO(e) => Display::fmt(e, f),
LogInError::PasswordTooLong => write!(f, "password must be no longer than {} bytes", MAX_OUTGOING_PAYLOAD_LEN),
LogInError::AlreadyLoggedIn => write!(f, "tried to log in when already logged in"),
LogInError::BadPassword => write!(f, "tried to log in with incorrect password")
}
}
}
impl Error for LogInError {}
#[derive(Debug)]
pub enum CommandError {
IO(io::Error),
CommandTooLong,
NotLoggedIn
}
impl From<io::Error> for CommandError {
fn from(e: io::Error) -> Self {
CommandError::IO(e)
}
}
impl From<SendError> for CommandError {
fn from(e: SendError) -> Self {
match e {
SendError::IO(e) => CommandError::IO(e),
SendError::PayloadTooLong => CommandError::CommandTooLong
}
}
}
impl Display for CommandError {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match self {
CommandError::IO(e) => Display::fmt(e, f),
CommandError::CommandTooLong => write!(f, "command must be no longer than {} bytes", MAX_OUTGOING_PAYLOAD_LEN),
CommandError::NotLoggedIn => write!(f, "tried to send a command before logging in")
}
}
}
impl Error for CommandError {}
#[derive(Debug)]
enum SendError {
IO(io::Error),
PayloadTooLong
}
impl From<io::Error> for SendError {
fn from(e: io::Error) -> Self {
SendError::IO(e)
}
}