valence_network/
legacy_ping.rs

1use std::io;
2use std::net::SocketAddr;
3use std::time::Duration;
4
5use tokio::io::{AsyncReadExt, AsyncWriteExt};
6use tokio::net::TcpStream;
7use tokio::time::sleep;
8
9use crate::{ServerListLegacyPing, SharedNetworkState};
10
11/// The payload of the legacy server list ping.
12#[derive(PartialEq, Debug, Clone)]
13pub enum ServerListLegacyPingPayload {
14    /// The 1.6 legacy ping format, which includes additional data.
15    Pre1_7 {
16        /// The protocol version of the client.
17        protocol: i32,
18        /// The hostname the client used to connect to the server.
19        hostname: String,
20        /// The port the client used to connect to the server.
21        port: u16,
22    },
23    /// The 1.4-1.5 legacy ping format.
24    Pre1_6,
25    /// The Beta 1.8-1.3 legacy ping format.
26    Pre1_4,
27}
28
29/// Response data of the legacy server list ping.
30///
31/// # Example
32///
33/// ```
34/// # use valence_network::ServerListLegacyPingResponse;
35/// let mut response =
36///     ServerListLegacyPingResponse::new(127, 0, 10).version("Valence 1.20.1".to_owned());
37///
38/// // This will make the description just repeat "hello" until the length limit
39/// // (which depends on the other fields that we set above: protocol, version,
40/// // online players, max players).
41/// let max_description = response.max_description();
42/// response = response.description(
43///     std::iter::repeat("hello ")
44///         .flat_map(|s| s.chars())
45///         .take(max_description)
46///         .collect(),
47/// );
48/// ```
49#[derive(Clone, Default, Debug, PartialEq)]
50pub struct ServerListLegacyPingResponse {
51    protocol: i32,
52    version: String,
53    online_players: i32,
54    max_players: i32,
55    description: String,
56}
57
58#[derive(PartialEq, Debug)]
59enum PingFormat {
60    Pre1_4, // Beta 1.8 to 1.3
61    Pre1_6, // 1.4 to 1.5
62    Pre1_7, // 1.6
63}
64
65/// Returns true if legacy ping detected and handled
66pub(crate) async fn try_handle_legacy_ping(
67    shared: &SharedNetworkState,
68    stream: &mut TcpStream,
69    remote_addr: SocketAddr,
70) -> io::Result<bool> {
71    let mut temp_buf = [0_u8; 3];
72    let mut n = stream.peek(&mut temp_buf).await?;
73
74    if let [0xfe] | [0xfe, 0x01] = &temp_buf[..n] {
75        // This could mean one of following things:
76        // 1. The beginning of a normal handshake packet, not fully received yet though
77        // 2. The beginning of the 1.6 legacy ping, not fully received yet either
78        // 3. Pre-1.4 legacy ping (0xfe) or 1.4-1.5 legacy ping (0xfe 0x01), fully
79        //    received
80        //
81        // So in the name of the Father, the Son, and the Holy Spirit, we pray,
82        // and wait for more data to arrive if it's 1 or 2, and if no
83        // data arrives for long enough, we can assume its 3.
84        //
85        // Downsides of this approach and where this could go wrong:
86        // 1. Short artificial delay for pre-1.4 and 1.4-1.5 legacy pings
87        // 2. If a normal handshake is encountered with the exact length of 0xfe 0x01 in
88        //    VarInt format (extremely rare, the server address would have to be ~248
89        //    bytes long), and for some God-forsaken reason sent the first 2 bytes of
90        //    the packet but not any more in this whole time, we would incorrectly
91        //    assume that it's a legacy ping and send an incorrect response.
92        // 3. If it was a 1.6 legacy ping, but even after the delay we only received
93        //    only 1 byte, then we would also send an incorrect response, thinking its a
94        //    pre-1.4 ping. The client would still understand it though, it'd just think
95        //    that the server is old (pre-1.4).
96        //
97        // In my opinion, 1 is insignificant, and 2/3 are so rare that they are
98        // effectively insignificant too. Network IO is just not that reliable
99        // at this level, the connection may be lost as well or something at this point.
100        sleep(Duration::from_millis(10)).await;
101        n = stream.peek(&mut temp_buf).await?;
102    }
103
104    let format = match &temp_buf[..n] {
105        [0xfe] => PingFormat::Pre1_4,
106        [0xfe, 0x01] => PingFormat::Pre1_6,
107        [0xfe, 0x01, 0xfa] => PingFormat::Pre1_7,
108        _ => return Ok(false), // Not a legacy ping
109    };
110
111    let payload = match format {
112        PingFormat::Pre1_7 => read_payload(stream).await?,
113        PingFormat::Pre1_6 => ServerListLegacyPingPayload::Pre1_6,
114        PingFormat::Pre1_4 => ServerListLegacyPingPayload::Pre1_4,
115    };
116
117    if let ServerListLegacyPing::Respond(mut response) = shared
118        .0
119        .callbacks
120        .inner
121        .server_list_legacy_ping(shared, remote_addr, payload)
122        .await
123    {
124        if format == PingFormat::Pre1_4 {
125            // remove formatting for pre-1.4 legacy pings
126            remove_formatting(&mut response.description);
127        }
128
129        let separator = match format {
130            PingFormat::Pre1_4 => '§',
131            _ => '\0',
132        };
133
134        let mut buf = Vec::new();
135
136        // packet ID and length placeholder
137        buf.extend([0xff, 0x00, 0x00]);
138
139        if format != PingFormat::Pre1_4 {
140            // some constant bytes lol
141            buf.extend("§1\0".encode_utf16().flat_map(|c| c.to_be_bytes()));
142
143            // protocol and version
144            buf.extend(
145                format!(
146                    "{protocol}{separator}{version}{separator}",
147                    protocol = response.protocol,
148                    version = response.version
149                )
150                .encode_utf16()
151                .flat_map(|c| c.to_be_bytes()),
152            );
153        }
154
155        // Description
156        buf.extend(
157            response
158                .description
159                .encode_utf16()
160                .flat_map(|c| c.to_be_bytes()),
161        );
162
163        // Online and max players
164        buf.extend(
165            format!(
166                "{separator}{online_players}{separator}{max_players}",
167                online_players = response.online_players,
168                max_players = response.max_players
169            )
170            .encode_utf16()
171            .flat_map(|c| c.to_be_bytes()),
172        );
173
174        // replace the length placeholder with the actual length
175        let chars = (buf.len() as u16 - 3) / 2; // -3 because of the packet prefix (id and length), and /2 because UTF16
176        buf[1..3].copy_from_slice(chars.to_be_bytes().as_slice());
177
178        stream.write_all(&buf).await?;
179    }
180
181    Ok(true)
182}
183
184// Reads the payload of a 1.6 legacy ping
185async fn read_payload(stream: &mut TcpStream) -> io::Result<ServerListLegacyPingPayload> {
186    // consume the first 29 useless bytes of this amazing protocol
187    stream.read_exact(&mut [0_u8; 29]).await?;
188
189    let protocol = i32::from(stream.read_u8().await?);
190    let hostname_len = usize::from(stream.read_u16().await?) * 2;
191
192    if hostname_len > 512 {
193        return Err(io::Error::new(
194            io::ErrorKind::InvalidData,
195            "hostname too long",
196        ));
197    }
198
199    let mut hostname = vec![0_u8; hostname_len];
200    stream.read_exact(&mut hostname).await?;
201    let hostname = String::from_utf16_lossy(
202        &hostname
203            .chunks(2)
204            .map(|pair| u16::from_be_bytes([pair[0], pair[1]]))
205            .collect::<Vec<_>>(),
206    );
207
208    let port = stream.read_i32().await? as u16;
209
210    Ok(ServerListLegacyPingPayload::Pre1_7 {
211        protocol,
212        hostname,
213        port,
214    })
215}
216
217impl ServerListLegacyPingResponse {
218    const MAX_VALID_LENGTH: usize = 248;
219
220    // Length of all the fields combined in string form. Used for validating and
221    // comparing with MAX_VALID_LENGTH.
222    fn length(&self) -> usize {
223        let mut len = 0;
224        len += int_len(self.protocol);
225        len += int_len(self.online_players);
226        len += int_len(self.max_players);
227        len += self.version.encode_utf16().count();
228        len += self.description.encode_utf16().count();
229
230        len
231    }
232    /// Constructs a new basic [`ServerListLegacyPingResponse`].
233    ///
234    /// See [`description`][Self::description] and [`version`][Self::version].
235    pub fn new(protocol: i32, online_players: i32, max_players: i32) -> Self {
236        Self {
237            protocol,
238            version: String::new(),
239            online_players,
240            max_players,
241            description: String::new(),
242        }
243    }
244    /// Sets the description of the server.
245    ///
246    /// If the resulting response packet is too long to be valid, the
247    /// description will be truncated.
248    ///
249    /// Use [`max_description`][Self::max_description] method to get the max
250    /// valid length for this specific packet with the already set fields
251    /// (version, protocol, online players, max players).
252    ///
253    /// Also any null bytes will be removed.
254    pub fn description(mut self, description: String) -> Self {
255        self.description = description;
256
257        self.description.retain(|c| c != '\0');
258
259        let overflow = self.length() as i32 - Self::MAX_VALID_LENGTH as i32;
260        if overflow > 0 {
261            let truncation_index = self
262                .description
263                .char_indices()
264                .nth(self.description.encode_utf16().count() - overflow as usize)
265                .unwrap()
266                .0;
267            self.description.truncate(truncation_index);
268        }
269
270        self
271    }
272    /// Sets the version of the server.
273    ///
274    /// If the resulting response packet is too long to be valid, the
275    /// version will be truncated.
276    ///
277    /// Use [`max_version`][Self::max_version] method to get the max valid
278    /// length for this specific packet with the already set fields
279    /// (description, protocol, online players, max players).
280    ///
281    /// Also any null bytes will be removed.
282    pub fn version(mut self, version: String) -> Self {
283        self.version = version;
284
285        self.version.retain(|c| c != '\0');
286
287        let overflow = self.length() as i32 - Self::MAX_VALID_LENGTH as i32;
288        if overflow > 0 {
289            let truncation_index = self
290                .version
291                .char_indices()
292                .nth(self.version.encode_utf16().count() - overflow as usize)
293                .unwrap()
294                .0;
295            self.version.truncate(truncation_index);
296        }
297
298        self
299    }
300    /// Returns the maximum number of characters (not bytes) that this packet's
301    /// description can have with all other fields set as they are.
302    pub fn max_description(&self) -> usize {
303        Self::MAX_VALID_LENGTH - (self.length() - self.description.encode_utf16().count())
304    }
305    /// Returns the maximum number of characters (not bytes) that this packet's
306    /// version can have with all other fields set as they are.
307    pub fn max_version(&self) -> usize {
308        Self::MAX_VALID_LENGTH - (self.length() - self.version.encode_utf16().count())
309    }
310}
311
312// Returns the length of a string representation of a signed integer
313fn int_len(num: i32) -> usize {
314    let num_abs = f64::from(num.abs());
315
316    if num < 0 {
317        (num_abs.log10() + 2.0) as usize // because minus sign
318    } else {
319        (num_abs.log10() + 1.0) as usize
320    }
321}
322
323// Removes all `§` and their modifiers, if any
324fn remove_formatting(string: &mut String) {
325    while let Some(pos) = string.find('§') {
326        // + 2 because we know that `§` is 2 bytes
327        if let Some(c) = string[(pos + 2)..].chars().next() {
328            // remove next char too if any
329            string.replace_range(pos..(pos + 2 + c.len_utf8()), "");
330        } else {
331            string.remove(pos);
332        }
333    }
334}