summaryrefslogtreecommitdiffstats
path: root/src/rcon.rs
blob: eb3b72a334aaf3a25546e0b8c41a6f65da3939f7 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
use std::io::{Read, Write};
use std::net::TcpStream;

#[derive(Debug, Clone)]
pub struct RconClient {
    host: String,
    port: u16,
    password: String,
}

#[derive(Debug)]
pub enum RconError {
    Connection(String),
    Auth(String),
    Response(String),
}

impl std::fmt::Display for RconError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            RconError::Connection(e) => write!(f, "RCON connection error: {e}"),
            RconError::Auth(e) => write!(f, "RCON auth error: {e}"),
            RconError::Response(e) => write!(f, "RCON response error: {e}"),
        }
    }
}

const SERVERDATA_AUTH: i32 = 3;
const SERVERDATA_EXECCOMMAND: i32 = 2;

impl RconClient {
    pub fn new(password: String) -> Self {
        Self {
            host: "127.0.0.1".to_string(),
            port: 25575,
            password,
        }
    }

    /// Non-blocking async wrapper — runs the blocking RCON call on a thread pool.
    pub async fn send_command_async(&self, command: String) -> Result<String, RconError> {
        let client = self.clone();
        tokio::task::spawn_blocking(move || client.send_command(&command))
            .await
            .map_err(|e| RconError::Connection(e.to_string()))?
    }

    fn send_command(&self, command: &str) -> Result<String, RconError> {
        let mut stream = TcpStream::connect(format!("{}:{}", self.host, self.port))
            .map_err(|e| RconError::Connection(e.to_string()))?;

        stream
            .set_read_timeout(Some(std::time::Duration::from_secs(1)))
            .ok();

        let request_id = 1;
        self.write_packet(&mut stream, request_id, SERVERDATA_AUTH, &self.password)?;
        let response = self.read_packet(&mut stream)?;

        if response.request_id == -1 {
            return Err(RconError::Auth("Invalid RCON password".into()));
        }

        let cmd_id = 2;
        self.write_packet(&mut stream, cmd_id, SERVERDATA_EXECCOMMAND, command)?;

        let response = self.read_packet(&mut stream)?;
        // Second packet is an end marker — some servers send it, some don't; ignore errors
        let _ = self.read_packet(&mut stream);

        Ok(response.body)
    }

    fn write_packet(
        &self,
        stream: &mut TcpStream,
        request_id: i32,
        packet_type: i32,
        body: &str,
    ) -> Result<(), RconError> {
        let body_bytes = body.as_bytes();
        let length = 10 + body_bytes.len() as i32;

        let mut packet = Vec::with_capacity(4 + length as usize);
        packet.extend_from_slice(&length.to_le_bytes());
        packet.extend_from_slice(&request_id.to_le_bytes());
        packet.extend_from_slice(&packet_type.to_le_bytes());
        packet.extend_from_slice(body_bytes);
        packet.extend_from_slice(&[0u8, 0u8]);

        stream
            .write_all(&packet)
            .map_err(|e| RconError::Connection(e.to_string()))?;

        Ok(())
    }

    fn read_packet(&self, stream: &mut TcpStream) -> Result<RconPacket, RconError> {
        let mut length_buf = [0u8; 4];
        stream
            .read_exact(&mut length_buf)
            .map_err(|e| RconError::Response(format!("Failed to read length: {e}")))?;

        let length = i32::from_le_bytes(length_buf);
        if length < 10 {
            return Err(RconError::Response("Packet too short".into()));
        }

        let mut payload = vec![0u8; length as usize];
        stream
            .read_exact(&mut payload)
            .map_err(|e| RconError::Response(format!("Failed to read payload: {e}")))?;

        let request_id = i32::from_le_bytes([payload[0], payload[1], payload[2], payload[3]]);

        let body_bytes = &payload[8..payload.len() - 2];
        let body = String::from_utf8_lossy(body_bytes).to_string();

        Ok(RconPacket { request_id, body })
    }
}

#[derive(Debug)]
struct RconPacket {
    request_id: i32,
    body: String,
}