ari mcstatusface

This commit is contained in:
ari melody 2025-04-14 07:20:05 +01:00
commit b0de168dda
Signed by: ari
GPG key ID: 60B5F0386E3DDB7E
7 changed files with 321 additions and 0 deletions

38
src/leb128.rs Normal file
View file

@ -0,0 +1,38 @@
const CONTINUE_BIT: u8 = 0x80;
pub struct LEB128;
impl LEB128 {
pub fn write_leb128(buffer: &mut Vec<u8>, 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;
}
}
}

4
src/lib.rs Normal file
View file

@ -0,0 +1,4 @@
pub mod leb128;
pub mod status;
pub use status::MinecraftStatus;

29
src/main.rs Normal file
View file

@ -0,0 +1,29 @@
use std::io::{Result};
use std::net::{ToSocketAddrs};
use std::env;
use mcstatusface::MinecraftStatus;
fn main() -> Result<()> {
let args: Vec<String> = env::args().collect();
if args.len() < 2 {
eprintln!("Usage: {} <address[:port]>", 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(())
}

144
src/status.rs Normal file
View file

@ -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<MinecraftPlayer>,
}
#[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<Vec<MinecraftDescriptionExtra>>,
bold: Option<bool>,
color: Option<String>,
}
#[derive(serde::Serialize, serde::Deserialize)]
pub struct MinecraftStatus {
pub version: MinecraftVersion,
pub players: MinecraftPlayers,
description: MinecraftDescription,
pub favicon: Option<String>,
#[serde(alias = "enforcesSecureChat")]
enforces_secure_chat: Option<bool>,
}
impl MinecraftStatus {
pub fn fetch(address: SocketAddr) -> Result<MinecraftStatus> {
// println!("Connecting to {address}...");
let mut stream = TcpStream::connect(address.to_string()).unwrap();
// println!("Connected!");
let mut send_buffer: Vec<u8> = 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<u8> = 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<u8> = Vec::new();
LEB128::write_leb128(&mut packet, data.len() as u64);
packet.extend_from_slice(&data);
stream.write(&packet).unwrap();
Ok(())
}