mc-minder 0.1.0

A smart management suite for Minecraft Fabric servers on Termux/Android
Documentation
use anyhow::{Result, Context};
use log::{info, debug};
use std::io::{Read, Write};
use std::net::TcpStream;
use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt};
use std::time::Duration;

pub struct RconClient {
    host: String,
    port: u16,
    password: String,
    stream: Option<TcpStream>,
}

const PACKET_TYPE_AUTH: i32 = 3;
const PACKET_TYPE_EXEC_COMMAND: i32 = 2;

struct Packet {
    id: i32,
    packet_type: i32,
    payload: String,
}

impl RconClient {
    pub fn new(host: String, port: u16, password: String) -> Self {
        Self {
            host,
            port,
            password,
            stream: None,
        }
    }

    pub fn connect(&mut self) -> Result<()> {
        let addr = format!("{}:{}", self.host, self.port);
        info!("Connecting to RCON at {}", addr);
        
        let stream = TcpStream::connect_timeout(
            &addr.parse().context("Invalid RCON address")?,
            Duration::from_secs(5),
        ).context("Failed to connect to RCON server")?;

        stream.set_read_timeout(Some(Duration::from_secs(10)))?;
        stream.set_write_timeout(Some(Duration::from_secs(10)))?;
        
        self.stream = Some(stream);
        
        self.authenticate()?;
        
        info!("RCON authenticated successfully");
        Ok(())
    }

    fn authenticate(&mut self) -> Result<()> {
        let packet = Packet {
            id: 1,
            packet_type: PACKET_TYPE_AUTH,
            payload: self.password.clone(),
        };
        
        self.send_packet(&packet)?;
        let response = self.read_packet()?;
        
        if response.id == -1 {
            anyhow::bail!("RCON authentication failed");
        }
        
        Ok(())
    }

    pub fn execute(&mut self, command: &str) -> Result<String> {
        if self.stream.is_none() {
            self.connect()?;
        }

        let packet = Packet {
            id: 1,
            packet_type: PACKET_TYPE_EXEC_COMMAND,
            payload: command.to_string(),
        };
        
        self.send_packet(&packet)?;
        let response = self.read_packet()?;
        
        Ok(response.payload)
    }

    fn send_packet(&mut self, packet: &Packet) -> Result<()> {
        let stream = self.stream.as_mut().context("Not connected to RCON")?;
        
        let payload_bytes = packet.payload.as_bytes();
        let length = 10 + payload_bytes.len() as i32;
        
        stream.write_i32::<BigEndian>(length)?;
        stream.write_i32::<BigEndian>(packet.id)?;
        stream.write_i32::<BigEndian>(packet.packet_type)?;
        stream.write_all(payload_bytes)?;
        stream.write_all(&[0, 0])?;
        stream.flush()?;
        
        debug!("Sent RCON packet: id={}, type={}", packet.id, packet.packet_type);
        Ok(())
    }

    fn read_packet(&mut self) -> Result<Packet> {
        let stream = self.stream.as_mut().context("Not connected to RCON")?;
        
        let length = stream.read_i32::<BigEndian>()?;
        let id = stream.read_i32::<BigEndian>()?;
        let packet_type = stream.read_i32::<BigEndian>()?;
        
        let payload_length = (length - 10) as usize;
        if payload_length > 4096 {
            anyhow::bail!("RCON packet too large");
        }
        
        let mut payload = vec![0u8; payload_length];
        stream.read_exact(&mut payload)?;
        
        let mut padding = [0u8; 2];
        stream.read_exact(&mut padding)?;
        
        let payload = String::from_utf8_lossy(&payload)
            .trim_end_matches('\0')
            .to_string();
        
        debug!("Received RCON packet: id={}, type={}, payload_len={}", id, packet_type, payload.len());
        
        Ok(Packet {
            id,
            packet_type,
            payload,
        })
    }

    pub fn disconnect(&mut self) {
        if let Some(stream) = self.stream.take() {
            let _ = stream.shutdown(std::net::Shutdown::Both);
            info!("Disconnected from RCON");
        }
    }

    pub fn say(&mut self, message: &str) -> Result<()> {
        let command = format!("say {}", message);
        self.execute(&command)?;
        Ok(())
    }

    pub fn tell(&mut self, player: &str, message: &str) -> Result<()> {
        let command = format!("tellraw {} {{\"text\":\"{}\"}}", player, message);
        self.execute(&command)?;
        Ok(())
    }
}

impl Drop for RconClient {
    fn drop(&mut self) {
        self.disconnect();
    }
}