Skip to main content

netget/server/minecraft/
wire.rs

1//! Minecraft Java Edition framing for the Server List Ping and the front of login: VarInt
2//! length-prefixed packets, the handshake, status JSON, ping/pong, Login Start, the login
3//! Disconnect, and the pre-1.7 legacy ping (`0xFE`) with its `0xFF` kick reply.
4//!
5//! Shared by the server and the client. Every length the peer announces is checked against a
6//! bound before anything is allocated for it.
7use anyhow::{bail, ensure, Context, Result};
8use serde_json::{json, Value};
9use std::time::Duration;
10use tokio::io::{AsyncRead, AsyncReadExt};
11
12/// Largest packet a client may send the server (handshake, Login Start, ping are all tiny).
13pub const MAX_SERVERBOUND_PACKET: usize = 2048;
14/// Largest packet the client accepts: a status response is one String of at most 32767 chars.
15pub const MAX_CLIENTBOUND_PACKET: usize = MAX_JSON_CHARS * 3 + 8;
16/// A Java `String` field holds at most 32767 UTF-16 units; the status JSON and a disconnect
17/// reason are both bounded by it.
18pub const MAX_JSON_CHARS: usize = 32767;
19/// Handshake server address, in characters.
20pub const MAX_ADDRESS_CHARS: usize = 255;
21/// Login Start player name, in characters.
22pub const MAX_USERNAME_CHARS: usize = 16;
23/// Players listed in a status sample.
24pub const MAX_SAMPLE: usize = 64;
25/// Deadline for each read and write, as the vanilla server's read timeout.
26pub const IO_TIMEOUT: Duration = Duration::from_secs(30);
27/// How long the legacy ping's optional tail (`01`, then `FA` + MC|PingHost) is waited for.
28pub const LEGACY_TAIL_WAIT: Duration = Duration::from_millis(300);
29/// Bytes of a 1.6 MC|PingHost payload accepted.
30pub const MAX_LEGACY_PAYLOAD: usize = 7 + MAX_ADDRESS_CHARS * 2;
31/// The version reported when the handler names none: 1.21.1.
32pub const DEFAULT_VERSION_NAME: &str = "1.21.1";
33pub const DEFAULT_PROTOCOL: i32 = 767;
34/// Protocol a 1.6 legacy ping carries.
35pub 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
53/// Decode a VarInt from the front of `buf`: (value, bytes used).
54pub 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
71/// A cursor over one packet's body.
72pub 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    /// A String field of at most `max_chars` characters (`max_chars * 3` bytes on the wire).
101    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
118/// A complete packet (length prefix included) from an id and body.
119pub 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
129/// Read a VarInt byte by byte. `None` on a clean EOF before the first byte.
130async 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
146/// Read one packet's bytes after its length prefix; the announced length is checked against
147/// `max` before any of it is read. `None` on a clean EOF between packets.
148pub 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
167/// Split an uncompressed packet into its id and body.
168pub 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
173/// Read one uncompressed packet's id and body. `None` on a clean EOF between packets.
174pub 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
220/// Login Start: the name, and the UUID when the version's layout carries one. The layout
221/// changed four times; the trailing bytes are read by shape rather than by version number.
222pub 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
235/// Login Start as a client of `protocol` sends it.
236pub 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        // 1.20.2+: the UUID, always present.
241        764.. => body.extend_from_slice(uuid),
242        // 1.19.3 – 1.20.1: optional UUID.
243        761..=763 => {
244            body.push(1);
245            body.extend_from_slice(uuid);
246        }
247        // 1.19.1/1.19.2: no signature data, no UUID.
248        760 => body.extend_from_slice(&[0, 0]),
249        // 1.19: no signature data.
250        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
281/// The status JSON for a `minecraft_status` action; `client_protocol` is unused unless the
282/// handler asks to echo it (`protocol: "echo"`).
283pub 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
331/// A login Disconnect reason as the JSON text component the client renders.
332pub 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
341/// Flatten a chat component (a string, an object with text/translate/extra, or an array) to
342/// plain text. Nesting beyond 32 levels is cut off rather than followed.
343pub 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/// What a legacy (`0xFE`) ping carried.
370#[derive(Debug, Clone, PartialEq, Eq)]
371pub struct LegacyPing {
372    /// `0xFE 0x01` (1.4 and later) rather than a bare `0xFE` (Beta 1.8 – 1.3).
373    pub modern_reply: bool,
374    /// From a 1.6 MC|PingHost payload, when one was sent.
375    pub protocol_version: Option<u8>,
376    pub server_address: Option<String>,
377    pub server_port: Option<u32>,
378}
379
380/// Read the rest of a legacy ping after its `0xFE`: the optional `0x01`, then the optional
381/// 1.6 `0xFA` MC|PingHost plugin message. Each tail is waited for briefly, as the vanilla
382/// server does, because older clients send nothing more.
383pub 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    // Some clients (mcstatus among them) send `FE 01 FA` and nothing more; answer them too.
401    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
448/// The 1.6 client's legacy ping: `FE 01 FA` + MC|PingHost naming `host:port`.
449pub 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
462/// The `0xFF` kick packet answering a legacy ping. 1.4+ clients get
463/// `§1\0protocol\0version\0motd\0online\0max`; a bare `0xFE` gets `motd§online§max`, where
464/// `§` is the separator and is therefore removed from the MOTD.
465pub 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
490/// Decode a `0xFF` kick reply (after the `0xFF` and the u16 length) into status fields.
491pub 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}