rkon 0.1.0

Yet another RCON library.
Documentation
//! Yet another RCON library.

use std::{
    io::{Read, Write},
    net::{TcpStream, ToSocketAddrs},
};

/// Maximum size of the RCON packet.
pub const MAX_PACKET_SIZE: usize = 4096;

/// Size of the RCON header, excluding the `size` field.
const HEADER_SIZE: usize = 10;

/// The error type for RCON operations.
#[derive(Debug)]
pub enum RconError {
    /// The server rejected the password.
    AuthenticationFail,
    /// Failed to parse the packet from the server.
    ParsingError,
    /// An IO error.
    IOError(std::io::Error),
}

impl From<std::io::Error> for RconError {
    fn from(value: std::io::Error) -> Self {
        Self::IOError(value)
    }
}

/// The kind of RCON packet.
#[derive(Debug, Clone, Copy)]
#[repr(i32)]
pub enum PacketType {
    /// Response to a Command.
    Response = 0,
    /// Command to run on the server.
    Command = 2,
    /// Authenticate with the server, needed before you can run any Command.
    Login = 3,
}

impl TryFrom<i32> for PacketType {
    type Error = RconError;

    fn try_from(value: i32) -> Result<Self, Self::Error> {
        match value {
            0 => Ok(Self::Response),
            2 => Ok(Self::Command),
            3 => Ok(Self::Login),
            _ => Err(RconError::ParsingError),
        }
    }
}

/// A RCON packet, that is used for both requests and responses.
#[derive(Debug)]
pub struct Packet {
    /// Request id sent by the client, which should be used in the response as well.
    pub request_id: i32,
    /// This packet's type.
    pub packet_type: PacketType,
    /// String content of this packet.
    pub body: String,
}

impl Packet {
    /// Encodes the packet into bytes.
    pub fn encode(&self) -> Vec<u8> {
        let mut bytes: Vec<u8> = vec![];

        let length = (self.body.len() + HEADER_SIZE) as i32;
        bytes.extend_from_slice(&length.to_le_bytes());
        bytes.extend_from_slice(&self.request_id.to_le_bytes());
        bytes.extend_from_slice(&(self.packet_type as i32).to_le_bytes());
        bytes.extend_from_slice(self.body.as_bytes());
        bytes.extend_from_slice(&[0, 0]);

        bytes
    }

    /// Decodes bytes into a packet.
    pub fn decode(bytes: &[u8]) -> Result<Self, RconError> {
        let size = i32::from_le_bytes(
            bytes[0..4]
                .try_into()
                .map_err(|_| RconError::ParsingError)?,
        );
        let id = i32::from_le_bytes(
            bytes[4..8]
                .try_into()
                .map_err(|_| RconError::ParsingError)?,
        );
        let msg_type = i32::from_le_bytes(
            bytes[8..12]
                .try_into()
                .map_err(|_| RconError::ParsingError)?,
        );

        let mut body = String::new();
        let body_len: usize = (size - HEADER_SIZE as i32)
            .try_into()
            .map_err(|_| RconError::ParsingError)?;
        if body_len > 0 {
            let body_bytes = std::str::from_utf8(&bytes[12..12 + body_len])
                .map_err(|_| RconError::ParsingError)?;
            body = body_bytes.to_string();
        }

        Ok(Self {
            request_id: id,
            packet_type: PacketType::try_from(msg_type)?,
            body,
        })
    }
}

/// A RCON client.
pub struct Client {
    stream: TcpStream,
    last_id: i32,
}

impl Client {
    /// Connect to the RCON server at `addr`, with `password`. The password must match what the server expects, otherwise it returns `AuthenticationFail`.
    pub fn connect<A: ToSocketAddrs>(addr: A, password: &str) -> Result<Self, RconError> {
        let mut client = Self {
            stream: TcpStream::connect(addr)?,
            last_id: 0,
        };

        let resp = client.send_packet(PacketType::Login, password)?;
        if resp.request_id == -1 {
            return Err(RconError::AuthenticationFail);
        }

        Ok(client)
    }

    fn send_packet(&mut self, packet_type: PacketType, body: &str) -> Result<Packet, RconError> {
        let packet = Packet {
            request_id: self.next_id(),
            packet_type,
            body: body.to_string(),
        };

        self.stream.write_all(&packet.encode())?;

        let mut resp_bytes = [0u8; MAX_PACKET_SIZE];
        self.stream.read(&mut resp_bytes)?;

        Packet::decode(&resp_bytes)
    }

    /// Send a command to the server, and returns the server's response.
    pub fn send(&mut self, command: &str) -> Result<String, RconError> {
        Ok(self.send_packet(PacketType::Command, command)?.body)
    }

    fn next_id(&mut self) -> i32 {
        let id = self.last_id;
        self.last_id += 1;
        id
    }
}