Skip to main content

rust_mc_status/protocol/
java_modern.rs

1//! Java Edition modern ping protocol (Minecraft 1.7+ / wire protocol 47+).
2//!
3//! Implements the [Server List Ping](https://wiki.vg/Server_List_Ping)
4//! handshake over TCP:
5//!
6//! ```text
7//! Client → Server: Handshake packet (0x00)
8//!   - Protocol version: 47
9//!   - Server address (hostname string)
10//!   - Server port
11//!   - Next state: 1 (Status)
12//!
13//! Client → Server: Status Request (0x00, no body)
14//!
15//! Server → Client: Status Response (0x00)
16//!   - JSON payload with version, players, description, favicon
17//! ```
18//!
19//! Connection is established directly (TCP), or tunnelled through a SOCKS5
20//! or HTTP CONNECT proxy when one is configured.
21//!
22//! # Unit struct design
23//!
24//! [`JavaModernProtocol`] is a zero-size unit struct — `Copy`, `Clone`, and
25//! `Default`.  No heap allocation occurs when dispatching a ping.
26
27use std::io::Cursor;
28use std::time::Duration;
29
30use serde_json::Value;
31use tokio::io::{AsyncReadExt, AsyncWriteExt};
32use tokio::net::TcpStream;
33use tokio::time::timeout;
34#[cfg(feature = "proxy")]
35use tokio_socks::tcp::Socks5Stream;
36
37use crate::core::time::{start_timer, elapsed_ms};
38use crate::error::McError;
39use crate::models::{JavaStatus, PingResult, ServerData};
40use crate::proxy::ProxyConfig;
41#[cfg(feature = "proxy")]
42use crate::proxy::ProxyKind;
43use super::{PingProtocol, ResolvedTarget};
44
45const PROTOCOL_VERSION: i32   = 47;
46const READ_BUFFER_SIZE: usize  = 4096;
47
48static STATUS_REQUEST: &[u8] = &[0x00];
49
50// ─── Protocol impl ────────────────────────────────────────────────────────────
51
52/// Java Edition modern ping (1.7+).
53///
54/// Unit struct — zero-size, `Copy`, no heap allocation.
55#[derive(Debug, Clone, Copy, Default)]
56pub struct JavaModernProtocol;
57
58impl PingProtocol for JavaModernProtocol {
59    fn name(&self) -> &'static str { "java-modern" }
60
61    async fn ping(
62        &self,
63        target:  &ResolvedTarget,
64        tout:    Duration,
65        proxy:   Option<&ProxyConfig>,
66    ) -> Result<PingResult, McError> {
67        let start = start_timer();
68        let mut stream = connect_tcp(target, proxy, tout).await?;
69        stream.set_nodelay(true).map_err(McError::Io)?;
70
71        send_packet(&mut stream, &build_handshake(&target.hostname, target.addr.port()), tout).await?;
72        send_packet(&mut stream, STATUS_REQUEST, tout).await?;
73
74        let (json, latency) = read_response(stream, start, tout).await?;
75        let data = ServerData::Java(JavaStatus::from_json(json)?);
76        Ok(PingResult::new(data, latency))
77    }
78}
79
80// ─── Connection (direct or SOCKS5) ───────────────────────────────────────────
81
82async fn connect_tcp(
83    target: &ResolvedTarget,
84    proxy:  Option<&ProxyConfig>,
85    tout:   Duration,
86) -> Result<TcpStream, McError> {
87    #[cfg(feature = "proxy")]
88    if let Some(cfg) = proxy {
89        let proxy_addr = format!("{}:{}", cfg.addr().host, cfg.addr().port);
90        return match cfg.kind() {
91            // ── SOCKS5 ────────────────────────────────────────────────────────
92            ProxyKind::Socks5 => {
93                let target_addr = (target.hostname.as_str(), target.addr.port());
94                let stream = match cfg.auth() {
95                    None => {
96                        timeout(tout, Socks5Stream::connect(proxy_addr.as_str(), target_addr))
97                            .await
98                            .map_err(|_| McError::timeout())?
99                            .map_err(|e| McError::proxy_error(e.to_string()))?
100                    }
101                    Some(auth) => {
102                        timeout(
103                            tout,
104                            Socks5Stream::connect_with_password(
105                                proxy_addr.as_str(),
106                                target_addr,
107                                &auth.username,
108                                &auth.password,
109                            ),
110                        )
111                        .await
112                        .map_err(|_| McError::timeout())?
113                        .map_err(|e| McError::proxy_error(e.to_string()))?
114                    }
115                };
116                Ok(stream.into_inner())
117            }
118
119            // ── HTTP CONNECT ──────────────────────────────────────────────────
120            ProxyKind::Http => {
121                crate::proxy::http::connect(
122                    &proxy_addr,
123                    &target.hostname,
124                    target.addr.port(),
125                    cfg.auth(),
126                    tout,
127                )
128                .await
129            }
130        };
131    }
132
133    // Direct connection — proxy is None or feature is disabled.
134    let _ = proxy;
135    timeout(tout, TcpStream::connect(target.addr))
136        .await
137        .map_err(|_| McError::timeout())?
138        .map_err(|e| McError::connection(e.to_string()))
139}
140
141// ─── Wire helpers ─────────────────────────────────────────────────────────────
142
143async fn send_packet(stream: &mut TcpStream, data: &[u8], tout: Duration) -> Result<(), McError> {
144    let mut packet = Vec::with_capacity(data.len() + 5);
145    write_varint(&mut packet, data.len() as i32);
146    packet.extend_from_slice(data);
147    timeout(tout, stream.write_all(&packet))
148        .await
149        .map_err(|_| McError::timeout())?
150        .map_err(McError::Io)
151}
152
153async fn read_response(
154    mut stream: TcpStream,
155    start:      tokio::time::Instant,
156    tout:       Duration,
157) -> Result<(Value, f64), McError> {
158    let mut resp     = Vec::new();
159    let mut buf      = [0u8; READ_BUFFER_SIZE];
160    let mut expected = None;
161
162    loop {
163        let n = timeout(tout, stream.read(&mut buf))
164            .await
165            .map_err(|_| McError::timeout())?
166            .map_err(McError::Io)?;
167        if n == 0 { break; }
168        resp.extend_from_slice(&buf[..n]);
169
170        if expected.is_none() && resp.len() >= 5 {
171            let mut cur = Cursor::new(&resp);
172            if let Ok(len) = read_varint(&mut cur) {
173                expected = Some(cur.position() as usize + len as usize);
174            }
175        }
176        if expected.is_some_and(|e| resp.len() >= e) { break; }
177    }
178
179    if resp.is_empty() {
180        return Err(McError::invalid_response("empty response"));
181    }
182
183    let mut cur      = Cursor::new(&resp);
184    let _pkt_len     = read_varint(&mut cur)?;
185    if read_varint(&mut cur)? != 0x00 {
186        return Err(McError::invalid_response("unexpected packet id"));
187    }
188    let json_len = read_varint(&mut cur)? as usize;
189    let pos      = cur.position() as usize;
190    let end      = pos
191        .checked_add(json_len)
192        .filter(|&e| e <= resp.len())
193        .ok_or_else(|| McError::invalid_response("json length exceeds packet"))?;
194
195    let json    = serde_json::from_slice(&resp[pos..end]).map_err(|e| McError::from(crate::error::ProtocolError::Json(e)))?;
196    let latency = elapsed_ms(start);
197    Ok((json, latency))
198}
199
200fn build_handshake(host: &str, port: u16) -> Vec<u8> {
201    let mut buf = Vec::with_capacity(7 + host.len());
202    write_varint(&mut buf, 0x00);
203    write_varint(&mut buf, PROTOCOL_VERSION);
204    write_varint(&mut buf, host.len() as i32);
205    buf.extend_from_slice(host.as_bytes());
206    buf.extend_from_slice(&port.to_be_bytes());
207    write_varint(&mut buf, 1);
208    buf
209}
210
211// ─── VarInt ──────────────────────────────────────────────────────────────────
212
213pub(crate) fn write_varint(buf: &mut Vec<u8>, v: i32) {
214    let mut v = v as u32;
215    while v >= 0x80 {
216        buf.push((v as u8 & 0x7F) | 0x80);
217        v >>= 7;
218    }
219    buf.push(v as u8);
220}
221
222pub(crate) fn read_varint<R: std::io::Read>(r: &mut R) -> Result<i32, McError> {
223    let (mut res, mut shift) = (0i32, 0u32);
224    loop {
225        let mut b = [0u8; 1];
226        r.read_exact(&mut b)
227            .map_err(|e| McError::invalid_response(e.to_string()))?;
228        let val = b[0] as i32;
229        res   |= (val & 0x7F) << shift;
230        shift += 7;
231        if shift > 35 {
232            return Err(McError::invalid_response("varint too long"));
233        }
234        if val & 0x80 == 0 { break; }
235    }
236    Ok(res)
237}