craftping/
lib.rs

1#![cfg_attr(docsrs, feature(doc_cfg))]
2#![allow(clippy::needless_doctest_main)]
3//! craftping provides a `ping` function to send Server List Ping requests to a Minecraft server.
4//!
5//! # Feature flags
6//!
7//! - `sync` (default): Enables synchronous, blocking [`ping`](crate::sync::ping) function.
8//! - `async-tokio`: Enables asynchronous, `tokio`-based [`ping`](crate::tokio::ping) function.
9//! - `async-futures`: Enables asynchronous, `futures`-based [`ping`](crate::futures::ping) function.
10//!
11//! # Examples
12//!
13//! ```no_run
14//! use craftping::sync::ping;
15//! use std::net::TcpStream;
16//!
17//! fn main() {
18//!     let hostname = "my.server.com";
19//!     let port = 25565;
20//!     let mut stream = TcpStream::connect((hostname, port)).unwrap();
21//!     let response = ping(&mut stream, hostname, port).unwrap();
22//!     println!("Players online: {}", response.online_players);
23//! }
24//! ```
25
26use std::{
27    fmt::Display,
28    io::{Read, Write},
29};
30
31mod entity;
32#[cfg(feature = "async-futures")]
33#[cfg_attr(docsrs, doc(cfg(feature = "async-futures")))]
34pub mod futures;
35#[cfg(feature = "sync")]
36#[cfg_attr(docsrs, doc(cfg(feature = "sync")))]
37pub mod sync;
38#[cfg(feature = "async-tokio")]
39#[cfg_attr(docsrs, doc(cfg(feature = "async-tokio")))]
40pub mod tokio;
41
42pub use entity::*;
43
44#[derive(Debug)]
45/// The ping error type.
46pub enum Error {
47    /// Returned when I/O (especially networking) failed.
48    Io(std::io::Error),
49    /// Returned when the response cannot be recognized.
50    UnsupportedProtocol,
51}
52
53impl Display for Error {
54    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
55        match self {
56            Self::Io(io) => io.fmt(f),
57            Self::UnsupportedProtocol => write!(f, "unsupported protocol"),
58        }
59    }
60}
61
62impl std::error::Error for Error {}
63
64impl From<std::io::Error> for Error {
65    fn from(error: std::io::Error) -> Self {
66        Self::Io(error)
67    }
68}
69
70/// The ping result type.
71pub type Result<T> = std::result::Result<T, Error>;
72
73fn build_latest_request(hostname: &str, port: u16) -> Result<Vec<u8>> {
74    // buffer for the 1st packet's data part
75    let mut buffer = vec![
76        0x00, // 1st packet id: 0 for handshake as VarInt
77        0xff, 0xff, 0xff, 0xff,
78        0x0f, // protocol version: -1 (determining what version to use) as VarInt
79    ];
80    // Some server implementations require hostname and port to be properly set (Notchian does not)
81    write_varint(&mut buffer, hostname.len() as i32); // length of hostname as VarInt
82    buffer.extend_from_slice(hostname.as_bytes());
83    buffer.extend_from_slice(&[
84        (port >> 8) as u8,
85        (port & 0b1111_1111) as u8, // server port as unsigned short
86        0x01,                       // next state: 1 (status) as VarInt
87    ]);
88    // buffer for the 1st and 2nd packet
89    let mut full_buffer = vec![];
90    write_varint(&mut full_buffer, buffer.len() as i32); // length of 1st packet id + data as VarInt
91    full_buffer.append(&mut buffer);
92    full_buffer.extend_from_slice(&[
93        1,    // length of 2nd packet id + data as VarInt
94        0x00, // 2nd packet id: 0 for request as VarInt
95    ]);
96    Ok(full_buffer)
97}
98
99fn decode_latest_response(buffer: &[u8]) -> Result<RawLatest> {
100    serde_json::from_slice(buffer).map_err(|_| Error::UnsupportedProtocol)
101}
102
103const LEGACY_REQUEST: [u8; 35] = [
104    0xfe, // 1st packet id: 0xfe for server list ping
105    0x01, // payload: always 1
106    0xfa, // 2nd packet id: 0xfa for plugin message
107    0x00, 0x0b, // length of following string: always 11 as short,
108    0x00, 0x4d, 0x00, 0x43, 0x00, 0x7c, 0x00, 0x50, 0x00, 0x69, 0x00, 0x6e, 0x00, 0x67, 0x00, 0x48,
109    0x00, 0x6f, 0x00, 0x73, 0x00, 0x74,
110    // MC|PingHost as UTF16-BE
111    7,    // length of the rest of the data: 7 + length of hostname
112    0x4a, // protocol version: 0x4a for the last version
113    0x00, 0x00, // length of hostname: 0 as short
114    0x00, 0x00, 0x00, 0x00, // port: 0 as int
115];
116
117fn decode_legacy(buffer: &[u8]) -> Result<String> {
118    if buffer.len() <= 3 || buffer[0] != 0xff {
119        return Err(Error::UnsupportedProtocol);
120    }
121    let utf16be: Vec<u16> = buffer[3..]
122        .chunks_exact(2)
123        .map(|chunk| ((chunk[0] as u16) << 8) | chunk[1] as u16)
124        .collect();
125    String::from_utf16(&utf16be).map_err(|_| Error::UnsupportedProtocol)
126}
127
128fn parse_legacy(s: &str, raw: Vec<u8>) -> Result<Response> {
129    let mut fields = s.split('\0');
130    let magic = fields.next().map(|s| s == "\u{00a7}\u{0031}");
131    let protocol = fields.next().and_then(|s| s.parse().ok());
132    let version = fields.next();
133    let motd = fields.next();
134    let players = fields.next().and_then(|s| s.parse().ok());
135    let max_players = fields.next().and_then(|s| s.parse().ok());
136    match (magic, protocol, version, motd, players, max_players) {
137        (
138            Some(true),
139            Some(protocol),
140            Some(version),
141            Some(motd),
142            Some(players),
143            Some(max_players),
144        ) => Ok(Response {
145            protocol,
146            enforces_secure_chat: None,
147            previews_chat: None,
148            version: version.to_string(),
149            description: Some(serde_json::Value::String(motd.to_string())),
150            online_players: players,
151            max_players,
152            favicon: None,
153            forge_data: None,
154            mod_info: None,
155            sample: None,
156            raw,
157        }),
158        _ => Err(Error::UnsupportedProtocol),
159    }
160}
161
162// used in read_varint implemenetation
163const LAST_SEVEN_BITS: i32 = 0b0111_1111;
164const NEXT_BYTE_EXISTS: u8 = 0b1000_0000;
165
166// bit mask to remove remaining 7 MSB's after right shift
167const SEVEN_BITS_SHIFT_MASK: i32 = 0x01_ff_ff_ff;
168
169fn write_varint(sink: &mut Vec<u8>, mut value: i32) {
170    loop {
171        let mut temp = (value & LAST_SEVEN_BITS) as u8;
172        // i32 right shift is arithmetic shift (preserves MSB)
173        value >>= 7;
174        value &= SEVEN_BITS_SHIFT_MASK;
175        if value != 0 {
176            temp |= NEXT_BYTE_EXISTS;
177        }
178        sink.push(temp);
179        if value == 0 {
180            break;
181        }
182    }
183}