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}