ari mcstatusface
This commit is contained in:
commit
b0de168dda
7 changed files with 321 additions and 0 deletions
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
/target
|
||||
.DS_Store
|
96
Cargo.lock
generated
Normal file
96
Cargo.lock
generated
Normal file
|
@ -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"
|
8
Cargo.toml
Normal file
8
Cargo.toml
Normal file
|
@ -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"
|
38
src/leb128.rs
Normal file
38
src/leb128.rs
Normal 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
4
src/lib.rs
Normal file
|
@ -0,0 +1,4 @@
|
|||
pub mod leb128;
|
||||
pub mod status;
|
||||
|
||||
pub use status::MinecraftStatus;
|
29
src/main.rs
Normal file
29
src/main.rs
Normal 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
144
src/status.rs
Normal 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(())
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue