commit b0de168ddafc7a760b7927b905602ebdffad82e6 Author: ari melody Date: Mon Apr 14 07:20:05 2025 +0100 ari mcstatusface diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0592392 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/target +.DS_Store diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..f36b30c --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,96 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + +[[package]] +name = "mcstatusface" +version = "0.1.0" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "proc-macro2" +version = "1.0.94" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + +[[package]] +name = "serde" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.140" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "syn" +version = "2.0.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..5b293e4 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "mcstatusface" +version = "0.1.0" +edition = "2024" + +[dependencies] +serde = { version = "1.0.219", features = ["derive"] } +serde_json = "1.0.140" diff --git a/src/leb128.rs b/src/leb128.rs new file mode 100644 index 0000000..7f7b091 --- /dev/null +++ b/src/leb128.rs @@ -0,0 +1,38 @@ +const CONTINUE_BIT: u8 = 0x80; + +pub struct LEB128; + +impl LEB128 { + pub fn write_leb128(buffer: &mut Vec, mut value: u64) -> usize { + let mut size: usize = 0; + loop { + let mut byte: u8 = (value & (std::u8::MAX as u64)).try_into().unwrap(); + value >>= 7; + if value != 0 { + byte |= CONTINUE_BIT; + } + + buffer.push(byte); + size += 1; + + if value == 0 { + return size; + } + } + } + + pub fn read_leb128(buffer: &[u8]) -> (u32, usize) { + let mut result: u32 = 0; + let mut shift: usize = 0; + let mut offset: usize = 0; + loop { + let byte: u8 = buffer[offset]; + result |= ((byte & (std::i8::MAX as u8)) as u32) << (shift as u32); + offset += 1; + if byte & CONTINUE_BIT == 0 || offset == 4 { + return (result, offset); + } + shift += 7; + } + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..e45878d --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,4 @@ +pub mod leb128; +pub mod status; + +pub use status::MinecraftStatus; diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..ec0cabc --- /dev/null +++ b/src/main.rs @@ -0,0 +1,29 @@ +use std::io::{Result}; +use std::net::{ToSocketAddrs}; +use std::env; + +use mcstatusface::MinecraftStatus; + +fn main() -> Result<()> { + let args: Vec = env::args().collect(); + if args.len() < 2 { + eprintln!("Usage: {} ", args[0]); + std::process::exit(1); + } + + let mut address = String::from(args[1].as_str()); + if !address.contains(":") { + address += ":25565"; + } + let mut addrs_iter = args[1].to_socket_addrs().unwrap(); + let address = addrs_iter.next().unwrap(); + + let status = MinecraftStatus::fetch(address).unwrap(); + + println!("\nVersion: {} ({})", status.version.name, status.version.protocol); + println!("Players: {}/{}", status.players.online, status.players.max); + println!("MOTD:"); + println!("{}", status.parse_description()); + + Ok(()) +} diff --git a/src/status.rs b/src/status.rs new file mode 100644 index 0000000..c0363ad --- /dev/null +++ b/src/status.rs @@ -0,0 +1,144 @@ +use std::io::{Error, ErrorKind, Read, Write, Result}; +use std::net::{SocketAddr, TcpStream}; + +use crate::leb128::LEB128; + +#[derive(serde::Serialize, serde::Deserialize)] +pub struct MinecraftVersion { + pub name: String, + pub protocol: u32, +} + +#[derive(serde::Serialize, serde::Deserialize)] +pub struct MinecraftPlayer { + pub name: String, + pub id: String, +} + +#[derive(serde::Serialize, serde::Deserialize)] +pub struct MinecraftPlayers { + pub online: u32, + pub max: u32, + pub sample: Vec, +} + +#[derive(serde::Serialize, serde::Deserialize)] +#[serde(untagged)] +enum MinecraftDescriptionExtra { + Extra(MinecraftDescription), + String(String), +} + +#[derive(serde::Serialize, serde::Deserialize)] +pub struct MinecraftDescription { + text: String, + extra: Option>, + bold: Option, + color: Option, +} + +#[derive(serde::Serialize, serde::Deserialize)] +pub struct MinecraftStatus { + pub version: MinecraftVersion, + pub players: MinecraftPlayers, + description: MinecraftDescription, + pub favicon: Option, + #[serde(alias = "enforcesSecureChat")] + enforces_secure_chat: Option, +} + +impl MinecraftStatus { + pub fn fetch(address: SocketAddr) -> Result { + // println!("Connecting to {address}..."); + + let mut stream = TcpStream::connect(address.to_string()).unwrap(); + // println!("Connected!"); + + let mut send_buffer: Vec = Vec::new(); + + // println!("Sending payload..."); + send_buffer.push(0x00); + LEB128::write_leb128(&mut send_buffer, 769); // 1.21.4 + LEB128::write_leb128(&mut send_buffer, address.ip().to_string().len().try_into().unwrap()); + send_buffer.extend_from_slice(address.ip().to_string().as_bytes()); + send_buffer.extend_from_slice(&address.port().to_be_bytes()); + LEB128::write_leb128(&mut send_buffer, 1); + send_packet(&mut stream, &send_buffer).unwrap(); + + send_packet(&mut stream, &[0x00]).unwrap(); + + let mut data: Vec = Vec::new(); + let mut len: usize = 0; + let mut msg_len: usize = 0; + let mut object_len: usize = 0; + let mut offset: usize = 0; + + loop { + let mut recv_buffer: [u8; 10240] = [0; 10240]; + len += stream.read(&mut recv_buffer)?; + + if len > 0 { + if msg_len == 0 { + let mut val: u32; + (val, offset) = LEB128::read_leb128(&recv_buffer); + msg_len = val as usize; + + if recv_buffer[offset] != 0x00 { + return Err(Error::new(ErrorKind::InvalidData, format!("Expected packet type 0x00, but got 0x{:02x?}!", recv_buffer[offset]))); + } + offset += 1; // skip message type bit + + let offset2: usize; + (val, offset2) = LEB128::read_leb128(&recv_buffer[offset..]); + object_len = val as usize; + offset += offset2; + } + data.extend_from_slice(&recv_buffer); + if len >= offset + object_len { + break; + } + } + } + let msg = std::str::from_utf8(&data[offset..]).unwrap().trim(); + let sanitised: String = msg.chars().filter(|&c| c >= '\u{20}' || c == '\n' || c == '\r' || c == '\t').collect(); + // println!("{sanitised}"); + let status: MinecraftStatus = serde_json::from_slice(sanitised.as_bytes()).unwrap(); + + Ok(status) + } + + pub fn parse_description(&self) -> String { + _description(&self.description) + } + + pub fn enforces_secure_chat(&self) -> bool { + self.enforces_secure_chat.unwrap_or(false) + } +} + +fn _description(description: &MinecraftDescription) -> String { + if description.extra.is_some() { + let mut extras = String::new(); + for extra in description.extra.as_ref().unwrap() { + match extra { + MinecraftDescriptionExtra::Extra(description) => { + extras += &_description(&description); + } + MinecraftDescriptionExtra::String(string) => { + extras += &string; + } + } + } + return description.text.clone() + &extras; + } + description.text.clone() +} + +fn send_packet(stream: &mut TcpStream, data: &[u8]) -> Result<()> { + let mut packet: Vec = Vec::new(); + LEB128::write_leb128(&mut packet, data.len() as u64); + packet.extend_from_slice(&data); + + stream.write(&packet).unwrap(); + Ok(()) +}