rust-mc-status 3.0.0

High-performance asynchronous Rust library for querying Minecraft server status (Java & Bedrock)
Documentation
//! Java Edition modern ping protocol (Minecraft 1.7+ / wire protocol 47+).
//!
//! Implements the [Server List Ping](https://wiki.vg/Server_List_Ping)
//! handshake over TCP:
//!
//! ```text
//! Client → Server: Handshake packet (0x00)
//!   - Protocol version: 47
//!   - Server address (hostname string)
//!   - Server port
//!   - Next state: 1 (Status)
//!
//! Client → Server: Status Request (0x00, no body)
//!
//! Server → Client: Status Response (0x00)
//!   - JSON payload with version, players, description, favicon
//! ```
//!
//! Connection is established directly (TCP), or tunnelled through a SOCKS5
//! or HTTP CONNECT proxy when one is configured.
//!
//! # Unit struct design
//!
//! [`JavaModernProtocol`] is a zero-size unit struct — `Copy`, `Clone`, and
//! `Default`.  No heap allocation occurs when dispatching a ping.

use std::io::Cursor;
use std::time::Duration;

use serde_json::Value;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::TcpStream;
use tokio::time::timeout;
#[cfg(feature = "proxy")]
use tokio_socks::tcp::Socks5Stream;

use crate::core::time::{start_timer, elapsed_ms};
use crate::error::McError;
use crate::models::{JavaStatus, PingResult, ServerData};
use crate::proxy::ProxyConfig;
#[cfg(feature = "proxy")]
use crate::proxy::ProxyKind;
use super::{PingProtocol, ResolvedTarget};

const PROTOCOL_VERSION: i32   = 47;
const READ_BUFFER_SIZE: usize  = 4096;

static STATUS_REQUEST: &[u8] = &[0x00];

// ─── Protocol impl ────────────────────────────────────────────────────────────

/// Java Edition modern ping (1.7+).
///
/// Unit struct — zero-size, `Copy`, no heap allocation.
#[derive(Debug, Clone, Copy, Default)]
pub struct JavaModernProtocol;

impl PingProtocol for JavaModernProtocol {
    fn name(&self) -> &'static str { "java-modern" }

    async fn ping(
        &self,
        target:  &ResolvedTarget,
        tout:    Duration,
        proxy:   Option<&ProxyConfig>,
    ) -> Result<PingResult, McError> {
        let start = start_timer();
        let mut stream = connect_tcp(target, proxy, tout).await?;
        stream.set_nodelay(true).map_err(McError::Io)?;

        send_packet(&mut stream, &build_handshake(&target.hostname, target.addr.port()), tout).await?;
        send_packet(&mut stream, STATUS_REQUEST, tout).await?;

        let (json, latency) = read_response(stream, start, tout).await?;
        let data = ServerData::Java(JavaStatus::from_json(json)?);
        Ok(PingResult::new(data, latency))
    }
}

// ─── Connection (direct or SOCKS5) ───────────────────────────────────────────

async fn connect_tcp(
    target: &ResolvedTarget,
    proxy:  Option<&ProxyConfig>,
    tout:   Duration,
) -> Result<TcpStream, McError> {
    #[cfg(feature = "proxy")]
    if let Some(cfg) = proxy {
        let proxy_addr = format!("{}:{}", cfg.addr().host, cfg.addr().port);
        return match cfg.kind() {
            // ── SOCKS5 ────────────────────────────────────────────────────────
            ProxyKind::Socks5 => {
                let target_addr = (target.hostname.as_str(), target.addr.port());
                let stream = match cfg.auth() {
                    None => {
                        timeout(tout, Socks5Stream::connect(proxy_addr.as_str(), target_addr))
                            .await
                            .map_err(|_| McError::timeout())?
                            .map_err(|e| McError::proxy_error(e.to_string()))?
                    }
                    Some(auth) => {
                        timeout(
                            tout,
                            Socks5Stream::connect_with_password(
                                proxy_addr.as_str(),
                                target_addr,
                                &auth.username,
                                &auth.password,
                            ),
                        )
                        .await
                        .map_err(|_| McError::timeout())?
                        .map_err(|e| McError::proxy_error(e.to_string()))?
                    }
                };
                Ok(stream.into_inner())
            }

            // ── HTTP CONNECT ──────────────────────────────────────────────────
            ProxyKind::Http => {
                crate::proxy::http::connect(
                    &proxy_addr,
                    &target.hostname,
                    target.addr.port(),
                    cfg.auth(),
                    tout,
                )
                .await
            }
        };
    }

    // Direct connection — proxy is None or feature is disabled.
    let _ = proxy;
    timeout(tout, TcpStream::connect(target.addr))
        .await
        .map_err(|_| McError::timeout())?
        .map_err(|e| McError::connection(e.to_string()))
}

// ─── Wire helpers ─────────────────────────────────────────────────────────────

async fn send_packet(stream: &mut TcpStream, data: &[u8], tout: Duration) -> Result<(), McError> {
    let mut packet = Vec::with_capacity(data.len() + 5);
    write_varint(&mut packet, data.len() as i32);
    packet.extend_from_slice(data);
    timeout(tout, stream.write_all(&packet))
        .await
        .map_err(|_| McError::timeout())?
        .map_err(McError::Io)
}

async fn read_response(
    mut stream: TcpStream,
    start:      tokio::time::Instant,
    tout:       Duration,
) -> Result<(Value, f64), McError> {
    let mut resp     = Vec::new();
    let mut buf      = [0u8; READ_BUFFER_SIZE];
    let mut expected = None;

    loop {
        let n = timeout(tout, stream.read(&mut buf))
            .await
            .map_err(|_| McError::timeout())?
            .map_err(McError::Io)?;
        if n == 0 { break; }
        resp.extend_from_slice(&buf[..n]);

        if expected.is_none() && resp.len() >= 5 {
            let mut cur = Cursor::new(&resp);
            if let Ok(len) = read_varint(&mut cur) {
                expected = Some(cur.position() as usize + len as usize);
            }
        }
        if expected.is_some_and(|e| resp.len() >= e) { break; }
    }

    if resp.is_empty() {
        return Err(McError::invalid_response("empty response"));
    }

    let mut cur      = Cursor::new(&resp);
    let _pkt_len     = read_varint(&mut cur)?;
    if read_varint(&mut cur)? != 0x00 {
        return Err(McError::invalid_response("unexpected packet id"));
    }
    let json_len = read_varint(&mut cur)? as usize;
    let pos      = cur.position() as usize;
    let end      = pos
        .checked_add(json_len)
        .filter(|&e| e <= resp.len())
        .ok_or_else(|| McError::invalid_response("json length exceeds packet"))?;

    let json    = serde_json::from_slice(&resp[pos..end]).map_err(|e| McError::from(crate::error::ProtocolError::Json(e)))?;
    let latency = elapsed_ms(start);
    Ok((json, latency))
}

fn build_handshake(host: &str, port: u16) -> Vec<u8> {
    let mut buf = Vec::with_capacity(7 + host.len());
    write_varint(&mut buf, 0x00);
    write_varint(&mut buf, PROTOCOL_VERSION);
    write_varint(&mut buf, host.len() as i32);
    buf.extend_from_slice(host.as_bytes());
    buf.extend_from_slice(&port.to_be_bytes());
    write_varint(&mut buf, 1);
    buf
}

// ─── VarInt ──────────────────────────────────────────────────────────────────

pub(crate) fn write_varint(buf: &mut Vec<u8>, v: i32) {
    let mut v = v as u32;
    while v >= 0x80 {
        buf.push((v as u8 & 0x7F) | 0x80);
        v >>= 7;
    }
    buf.push(v as u8);
}

pub(crate) fn read_varint<R: std::io::Read>(r: &mut R) -> Result<i32, McError> {
    let (mut res, mut shift) = (0i32, 0u32);
    loop {
        let mut b = [0u8; 1];
        r.read_exact(&mut b)
            .map_err(|e| McError::invalid_response(e.to_string()))?;
        let val = b[0] as i32;
        res   |= (val & 0x7F) << shift;
        shift += 7;
        if shift > 35 {
            return Err(McError::invalid_response("varint too long"));
        }
        if val & 0x80 == 0 { break; }
    }
    Ok(res)
}