1use anyhow::{bail, ensure, Context, Result};
8use serde_json::{json, Value};
9use std::time::Duration;
10use tokio::io::{AsyncRead, AsyncReadExt};
11
12pub const MAX_SERVERBOUND_PACKET: usize = 2048;
14pub const MAX_CLIENTBOUND_PACKET: usize = MAX_JSON_CHARS * 3 + 8;
16pub const MAX_JSON_CHARS: usize = 32767;
19pub const MAX_ADDRESS_CHARS: usize = 255;
21pub const MAX_USERNAME_CHARS: usize = 16;
23pub const MAX_SAMPLE: usize = 64;
25pub const IO_TIMEOUT: Duration = Duration::from_secs(30);
27pub const LEGACY_TAIL_WAIT: Duration = Duration::from_millis(300);
29pub const MAX_LEGACY_PAYLOAD: usize = 7 + MAX_ADDRESS_CHARS * 2;
31pub const DEFAULT_VERSION_NAME: &str = "1.21.1";
33pub const DEFAULT_PROTOCOL: i32 = 767;
34pub const LEGACY_PING_PROTOCOL: u8 = 74;
36
37pub const STATE_STATUS: i32 = 1;
38pub const STATE_LOGIN: i32 = 2;
39pub const STATE_TRANSFER: i32 = 3;
40
41pub fn put_varint(out: &mut Vec<u8>, value: i32) {
42 let mut v = value as u32;
43 loop {
44 if v & !0x7f == 0 {
45 out.push(v as u8);
46 return;
47 }
48 out.push((v as u8 & 0x7f) | 0x80);
49 v >>= 7;
50 }
51}
52
53pub fn get_varint(buf: &[u8]) -> Result<(i32, usize)> {
55 let mut value = 0u32;
56 for (i, byte) in buf.iter().take(5).enumerate() {
57 value |= u32::from(byte & 0x7f) << (7 * i);
58 if byte & 0x80 == 0 {
59 return Ok((value as i32, i + 1));
60 }
61 }
62 ensure!(buf.len() >= 5, "truncated VarInt");
63 bail!("VarInt longer than 5 bytes")
64}
65
66pub fn put_string(out: &mut Vec<u8>, s: &str) {
67 put_varint(out, s.len() as i32);
68 out.extend_from_slice(s.as_bytes());
69}
70
71pub struct Reader<'a> {
73 buf: &'a [u8],
74}
75
76impl<'a> Reader<'a> {
77 pub fn new(buf: &'a [u8]) -> Self {
78 Self { buf }
79 }
80 pub fn remaining(&self) -> &'a [u8] {
81 self.buf
82 }
83 pub fn varint(&mut self) -> Result<i32> {
84 let (v, n) = get_varint(self.buf)?;
85 self.buf = &self.buf[n..];
86 Ok(v)
87 }
88 pub fn bytes(&mut self, n: usize) -> Result<&'a [u8]> {
89 ensure!(self.buf.len() >= n, "packet ends inside a field");
90 let (head, tail) = self.buf.split_at(n);
91 self.buf = tail;
92 Ok(head)
93 }
94 pub fn u16(&mut self) -> Result<u16> {
95 Ok(u16::from_be_bytes(self.bytes(2)?.try_into()?))
96 }
97 pub fn i64(&mut self) -> Result<i64> {
98 Ok(i64::from_be_bytes(self.bytes(8)?.try_into()?))
99 }
100 pub fn string(&mut self, max_chars: usize) -> Result<String> {
102 let len = self.varint()?;
103 ensure!(
104 len >= 0 && len as usize <= max_chars * 3,
105 "string of {len} bytes exceeds {max_chars} characters"
106 );
107 let s = std::str::from_utf8(self.bytes(len as usize)?)
108 .context("string is not UTF-8")?
109 .to_string();
110 ensure!(
111 s.encode_utf16().count() <= max_chars,
112 "string exceeds {max_chars} characters"
113 );
114 Ok(s)
115 }
116}
117
118pub fn frame(id: i32, body: &[u8]) -> Vec<u8> {
120 let mut inner = Vec::with_capacity(body.len() + 5);
121 put_varint(&mut inner, id);
122 inner.extend_from_slice(body);
123 let mut out = Vec::with_capacity(inner.len() + 5);
124 put_varint(&mut out, inner.len() as i32);
125 out.extend_from_slice(&inner);
126 out
127}
128
129async fn read_varint<R: AsyncRead + Unpin>(reader: &mut R) -> Result<Option<i32>> {
131 let mut value = 0u32;
132 for i in 0..5 {
133 let byte = match reader.read_u8().await {
134 Ok(b) => b,
135 Err(e) if i == 0 && e.kind() == std::io::ErrorKind::UnexpectedEof => return Ok(None),
136 Err(e) => return Err(e.into()),
137 };
138 value |= u32::from(byte & 0x7f) << (7 * i);
139 if byte & 0x80 == 0 {
140 return Ok(Some(value as i32));
141 }
142 }
143 bail!("VarInt longer than 5 bytes")
144}
145
146pub async fn read_raw<R: AsyncRead + Unpin>(
149 reader: &mut R,
150 max: usize,
151 deadline: Duration,
152) -> Result<Option<Vec<u8>>> {
153 tokio::time::timeout(deadline, async {
154 let Some(len) = read_varint(reader).await? else {
155 return Ok(None);
156 };
157 ensure!(len > 0, "packet length {len} is not positive");
158 ensure!(len as usize <= max, "packet of {len} bytes exceeds {max}");
159 let mut buf = vec![0u8; len as usize];
160 reader.read_exact(&mut buf).await?;
161 Ok(Some(buf))
162 })
163 .await
164 .context("Minecraft read deadline")?
165}
166
167pub fn split_id(buf: &[u8]) -> Result<(i32, Vec<u8>)> {
169 let (id, n) = get_varint(buf)?;
170 Ok((id, buf[n..].to_vec()))
171}
172
173pub async fn read_packet<R: AsyncRead + Unpin>(
175 reader: &mut R,
176 max: usize,
177 deadline: Duration,
178) -> Result<Option<(i32, Vec<u8>)>> {
179 match read_raw(reader, max, deadline).await? {
180 Some(buf) => Ok(Some(split_id(&buf)?)),
181 None => Ok(None),
182 }
183}
184
185#[derive(Debug, Clone, PartialEq, Eq)]
186pub struct Handshake {
187 pub protocol_version: i32,
188 pub server_address: String,
189 pub server_port: u16,
190 pub next_state: i32,
191}
192
193pub fn parse_handshake(body: &[u8]) -> Result<Handshake> {
194 let mut r = Reader::new(body);
195 let protocol_version = r.varint()?;
196 let server_address = r.string(MAX_ADDRESS_CHARS)?;
197 let server_port = r.u16()?;
198 let next_state = r.varint()?;
199 ensure!(
200 matches!(next_state, STATE_STATUS | STATE_LOGIN | STATE_TRANSFER),
201 "handshake asks for unknown state {next_state}"
202 );
203 Ok(Handshake {
204 protocol_version,
205 server_address,
206 server_port,
207 next_state,
208 })
209}
210
211pub fn encode_handshake(h: &Handshake) -> Vec<u8> {
212 let mut body = Vec::new();
213 put_varint(&mut body, h.protocol_version);
214 put_string(&mut body, &h.server_address);
215 body.extend_from_slice(&h.server_port.to_be_bytes());
216 put_varint(&mut body, h.next_state);
217 frame(0x00, &body)
218}
219
220pub fn parse_login_start(body: &[u8]) -> Result<(String, Option<String>)> {
223 let mut r = Reader::new(body);
224 let name = r.string(MAX_USERNAME_CHARS)?;
225 ensure!(!name.is_empty(), "empty player name");
226 let rest = r.remaining();
227 let uuid = match rest.len() {
228 16 => Some(format_uuid(rest)),
229 17 if rest[0] == 1 => Some(format_uuid(&rest[1..])),
230 _ => None,
231 };
232 Ok((name, uuid))
233}
234
235pub fn encode_login_start(protocol: i32, name: &str, uuid: &[u8; 16]) -> Vec<u8> {
237 let mut body = Vec::new();
238 put_string(&mut body, name);
239 match protocol {
240 764.. => body.extend_from_slice(uuid),
242 761..=763 => {
244 body.push(1);
245 body.extend_from_slice(uuid);
246 }
247 760 => body.extend_from_slice(&[0, 0]),
249 759 => body.push(0),
251 _ => {}
252 }
253 frame(0x00, &body)
254}
255
256pub fn format_uuid(b: &[u8]) -> String {
257 let hex: String = b.iter().map(|x| format!("{x:02x}")).collect();
258 format!(
259 "{}-{}-{}-{}-{}",
260 &hex[0..8],
261 &hex[8..12],
262 &hex[12..16],
263 &hex[16..20],
264 &hex[20..32]
265 )
266}
267
268pub fn parse_uuid(s: &str) -> Result<[u8; 16]> {
269 let hex: String = s.chars().filter(|c| *c != '-').collect();
270 ensure!(
271 hex.len() == 32 && hex.chars().all(|c| c.is_ascii_hexdigit()),
272 "uuid must be 32 hex digits, optionally hyphenated"
273 );
274 let mut out = [0u8; 16];
275 for (i, byte) in out.iter_mut().enumerate() {
276 *byte = u8::from_str_radix(&hex[i * 2..i * 2 + 2], 16)?;
277 }
278 Ok(out)
279}
280
281pub fn status_json(answer: &Value, client_protocol: i32) -> Result<String> {
284 let name = answer["version_name"]
285 .as_str()
286 .unwrap_or(DEFAULT_VERSION_NAME);
287 let protocol = match &answer["protocol"] {
288 Value::Null => DEFAULT_PROTOCOL as i64,
289 Value::String(s) if s == "echo" => client_protocol as i64,
290 v => v
291 .as_i64()
292 .context("protocol must be a number or \"echo\"")?,
293 };
294 let max = answer["max_players"].as_i64().unwrap_or(20);
295 let online = answer["online_players"].as_i64().unwrap_or(0);
296 let mut players = json!({"max": max, "online": online});
297 if let Some(sample) = answer["sample"].as_array() {
298 ensure!(
299 sample.len() <= MAX_SAMPLE,
300 "sample lists more than {MAX_SAMPLE} players"
301 );
302 let mut out = Vec::new();
303 for p in sample {
304 let player = p["name"]
305 .as_str()
306 .context("each sample entry needs a name")?;
307 let id = match p["id"].as_str() {
308 Some(id) => format_uuid(&parse_uuid(id)?),
309 None => "00000000-0000-0000-0000-000000000000".to_string(),
310 };
311 out.push(json!({"name": player, "id": id}));
312 }
313 players["sample"] = Value::Array(out);
314 }
315 let mut status = json!({
316 "version": {"name": name, "protocol": protocol},
317 "players": players,
318 "description": {"text": answer["motd"].as_str().unwrap_or("")},
319 });
320 if let Some(flag) = answer["enforces_secure_chat"].as_bool() {
321 status["enforcesSecureChat"] = Value::Bool(flag);
322 }
323 let text = status.to_string();
324 ensure!(
325 text.encode_utf16().count() <= MAX_JSON_CHARS,
326 "status JSON exceeds {MAX_JSON_CHARS} characters"
327 );
328 Ok(text)
329}
330
331pub fn disconnect_json(reason: &str) -> Result<String> {
333 let text = json!({"text": reason}).to_string();
334 ensure!(
335 text.encode_utf16().count() <= MAX_JSON_CHARS,
336 "disconnect reason exceeds {MAX_JSON_CHARS} characters"
337 );
338 Ok(text)
339}
340
341pub fn plain_text(component: &Value) -> String {
344 fn walk(v: &Value, depth: usize, out: &mut String) {
345 if depth > 32 {
346 return;
347 }
348 match v {
349 Value::String(s) => out.push_str(s),
350 Value::Array(items) => items.iter().for_each(|i| walk(i, depth + 1, out)),
351 Value::Object(map) => {
352 if let Some(t) = map.get("text").and_then(Value::as_str) {
353 out.push_str(t);
354 } else if let Some(t) = map.get("translate").and_then(Value::as_str) {
355 out.push_str(t);
356 }
357 if let Some(extra) = map.get("extra") {
358 walk(extra, depth + 1, out);
359 }
360 }
361 _ => {}
362 }
363 }
364 let mut out = String::new();
365 walk(component, 0, &mut out);
366 out
367}
368
369#[derive(Debug, Clone, PartialEq, Eq)]
371pub struct LegacyPing {
372 pub modern_reply: bool,
374 pub protocol_version: Option<u8>,
376 pub server_address: Option<String>,
377 pub server_port: Option<u32>,
378}
379
380pub async fn read_legacy_ping<R: AsyncRead + Unpin>(reader: &mut R) -> Result<LegacyPing> {
384 let mut ping = LegacyPing {
385 modern_reply: false,
386 protocol_version: None,
387 server_address: None,
388 server_port: None,
389 };
390 match tokio::time::timeout(LEGACY_TAIL_WAIT, reader.read_u8()).await {
391 Ok(Ok(0x01)) => ping.modern_reply = true,
392 Ok(Ok(other)) => bail!("legacy ping followed by 0x{other:02x}"),
393 Ok(Err(_)) | Err(_) => return Ok(ping),
394 }
395 match tokio::time::timeout(LEGACY_TAIL_WAIT, reader.read_u8()).await {
396 Ok(Ok(0xFA)) => {}
397 Ok(Ok(other)) => bail!("legacy ping tail starts with 0x{other:02x}"),
398 Ok(Err(_)) | Err(_) => return Ok(ping),
399 }
400 let first = match tokio::time::timeout(LEGACY_TAIL_WAIT, reader.read_u8()).await {
402 Ok(Ok(b)) => b,
403 Ok(Err(_)) | Err(_) => return Ok(ping),
404 };
405 tokio::time::timeout(IO_TIMEOUT, async {
406 let channel_len = u16::from_be_bytes([first, reader.read_u8().await?]) as usize;
407 ensure!(
408 channel_len == 11,
409 "legacy plugin channel is not MC|PingHost"
410 );
411 let mut channel = vec![0u8; channel_len * 2];
412 reader.read_exact(&mut channel).await?;
413 ensure!(
414 utf16be(&channel)? == "MC|PingHost",
415 "legacy plugin channel is not MC|PingHost"
416 );
417 let len = reader.read_u16().await? as usize;
418 ensure!(
419 (7..=MAX_LEGACY_PAYLOAD).contains(&len),
420 "MC|PingHost payload of {len} bytes"
421 );
422 let mut payload = vec![0u8; len];
423 reader.read_exact(&mut payload).await?;
424 let mut r = Reader::new(&payload);
425 let protocol = r.bytes(1)?[0];
426 let host_len = r.u16()? as usize;
427 ensure!(host_len <= MAX_ADDRESS_CHARS, "MC|PingHost host too long");
428 let host = utf16be(r.bytes(host_len * 2)?)?;
429 let port = u32::from_be_bytes(r.bytes(4)?.try_into()?);
430 ping.protocol_version = Some(protocol);
431 ping.server_address = Some(host);
432 ping.server_port = Some(port);
433 Ok(ping)
434 })
435 .await
436 .context("legacy ping deadline")?
437}
438
439fn utf16be(bytes: &[u8]) -> Result<String> {
440 ensure!(bytes.len().is_multiple_of(2), "odd UTF-16 length");
441 let units: Vec<u16> = bytes
442 .chunks(2)
443 .map(|c| u16::from_be_bytes([c[0], c[1]]))
444 .collect();
445 String::from_utf16(&units).context("invalid UTF-16")
446}
447
448pub fn encode_legacy_ping(host: &str, port: u16) -> Vec<u8> {
450 let mut out = vec![0xFE, 0x01, 0xFA];
451 out.extend_from_slice(&11u16.to_be_bytes());
452 out.extend("MC|PingHost".encode_utf16().flat_map(u16::to_be_bytes));
453 let host16: Vec<u8> = host.encode_utf16().flat_map(u16::to_be_bytes).collect();
454 out.extend_from_slice(&((7 + host16.len()) as u16).to_be_bytes());
455 out.push(LEGACY_PING_PROTOCOL);
456 out.extend_from_slice(&((host16.len() / 2) as u16).to_be_bytes());
457 out.extend_from_slice(&host16);
458 out.extend_from_slice(&u32::from(port).to_be_bytes());
459 out
460}
461
462pub fn encode_legacy_reply(ping: &LegacyPing, answer: &Value) -> Result<Vec<u8>> {
466 let motd = answer["motd"].as_str().unwrap_or("");
467 let online = answer["online_players"].as_i64().unwrap_or(0);
468 let max = answer["max_players"].as_i64().unwrap_or(20);
469 let text = if ping.modern_reply {
470 let protocol = match &answer["protocol"] {
471 Value::Number(n) => n.to_string(),
472 _ => ping
473 .protocol_version
474 .unwrap_or(LEGACY_PING_PROTOCOL)
475 .to_string(),
476 };
477 let name = answer["version_name"].as_str().unwrap_or("1.6.4");
478 format!("§1\0{protocol}\0{name}\0{motd}\0{online}\0{max}")
479 } else {
480 format!("{}§{online}§{max}", motd.replace('§', ""))
481 };
482 let units: Vec<u16> = text.encode_utf16().collect();
483 ensure!(units.len() <= MAX_JSON_CHARS, "legacy reply too long");
484 let mut out = vec![0xFF];
485 out.extend_from_slice(&(units.len() as u16).to_be_bytes());
486 out.extend(units.iter().flat_map(|u| u.to_be_bytes()));
487 Ok(out)
488}
489
490pub fn decode_legacy_reply(text_utf16: &[u8]) -> Result<Value> {
492 let text = utf16be(text_utf16)?;
493 if let Some(rest) = text.strip_prefix("§1\0") {
494 let f: Vec<&str> = rest.split('\0').collect();
495 ensure!(f.len() == 5, "legacy reply has {} fields", f.len());
496 return Ok(json!({
497 "protocol": f[0].parse::<i64>().ok(),
498 "version_name": f[1],
499 "motd": f[2],
500 "online_players": f[3].parse::<i64>().ok(),
501 "max_players": f[4].parse::<i64>().ok(),
502 }));
503 }
504 let f: Vec<&str> = text.rsplitn(3, '§').collect();
505 ensure!(f.len() == 3, "legacy reply is not motd§online§max");
506 Ok(json!({
507 "protocol": null,
508 "version_name": null,
509 "motd": f[2],
510 "online_players": f[1].parse::<i64>().ok(),
511 "max_players": f[0].parse::<i64>().ok(),
512 }))
513}