From 72627d2f6efe3f3297ce466a1fdd208015a19f88 Mon Sep 17 00:00:00 2001 From: Plex Date: Wed, 15 Jun 2022 17:22:59 +0200 Subject: mcquery --- src/bin/mcquery.rs | 314 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 314 insertions(+) create mode 100644 src/bin/mcquery.rs (limited to 'src/bin/mcquery.rs') diff --git a/src/bin/mcquery.rs b/src/bin/mcquery.rs new file mode 100644 index 0000000..8dcd1bc --- /dev/null +++ b/src/bin/mcquery.rs @@ -0,0 +1,314 @@ +extern crate getopts; +use getopts::Options; +use std::env; +use rand::Rng; +use std::net::{UdpSocket,Ipv4Addr,ToSocketAddrs,SocketAddr}; + +pub mod querynet { + use std::net::{UdpSocket, SocketAddr}; + use std::str; + use bytes::{BytesMut, BufMut}; + use byteorder::{ByteOrder,LittleEndian}; + use std::collections::HashMap; + use json::object; + + pub struct BasicStat { + pub motd: String, + pub gametype: String, + pub map: String, + pub numplayers: u32, + pub maxplayers: u32, + pub hostport: u16, + pub hostip: String + } + + impl BasicStat { + pub fn ip(&self) { + println!("{}:{}", self.hostip, self.hostport); + } + + pub fn players(&self) { + println!("{}/{}", self.numplayers, self.maxplayers); + } + + pub fn info(&self) { + println!("{}\n{}, {}", self.motd, self.map, self.gametype) + } + + pub fn json(&self) -> String { + object!{ + motd: self.motd.clone(), + gametype: self.gametype.clone(), + map: self.map.clone(), + players: object!{ + max: self.maxplayers, + online: self.numplayers, + }, + hostport: self.hostport, + hostip: self.hostip.clone() + }.dump() + } + + } + + pub struct FullStat { + pub motd: String, + pub gametype: String, + pub game_id: String, + pub version: String, + pub plugins: String, // later? + pub map: String, + pub numplayers: u32, + pub maxplayers: u32, + pub hostport: u16, + pub hostip: String, + pub players: Vec, + } + + impl FullStat { + + pub fn set_players(&mut self, players: Vec) { + self.players = players; + } + + pub fn json(&self) -> String { + object!{ + motd: self.motd.clone(), + gametype: self.gametype.clone(), + gameid: self.game_id.clone(), + version: self.version.clone(), + plugins: self.plugins.clone(), + map: self.map.clone(), + players: object!{ + max: self.maxplayers, + online: self.numplayers, + list: self.players.clone() + }, + hostport: self.hostport, + hostip: self.hostip.clone() + }.dump() + } + } + + pub fn convert_kv(kv: HashMap) -> FullStat { + FullStat { + motd: match kv.get(&String::from("hostname")) { Some(v) => v.clone(), _ => String::from("") }, + gametype: match kv.get(&String::from("gametype")) { Some(v) => v.clone(), _ => String::from("") }, + game_id: match kv.get(&String::from("game_id")) { Some(v) => v.clone(), _ => String::from("") }, + version: match kv.get(&String::from("version")) { Some(v) => v.clone(), _ => String::from("") }, + plugins: match kv.get(&String::from("plugins")) { Some(v) => v.clone(), _ => String::from("") }, + map: match kv.get(&String::from("map")) { Some(v) => v.clone(), _ => String::from("") }, + numplayers: match kv.get(&String::from("numplayers")) { Some(v) => v.clone().parse().expect("NaN"), _ => 0 }, + maxplayers: match kv.get(&String::from("maxplayers")) { Some(v) => v.clone().parse().expect("NaN"), _ => 0 }, + hostport: match kv.get(&String::from("hostport")) { Some(v) => v.clone().parse().expect("NaN"), _ => 0 }, + hostip: match kv.get(&String::from("hostip")) { Some(v) => v.clone(), _ => String::from("") }, + players: Vec::new() + } + } + + + pub fn slice_to_string(slice: &[u8]) -> String { + String::from_utf16(&slice.iter().map(|&slice| slice as u16).collect::>()[..]).expect("String is not UTF-16, for some reason.") + } + + pub fn send_packet(socket: &UdpSocket, addr: SocketAddr, ptype: u8, session_id: u32, payload: &[u8]) { + let mut packet = BytesMut::with_capacity(16384); + packet.put(&b"\xFE\xFD"[..]); + packet.put_u8(ptype); + packet.put_u32(session_id); + packet.put(payload); + socket.send_to(&packet, addr).expect("Couldn't send packet."); + } + + pub fn recv_packet(socket: &UdpSocket) -> BytesMut { + let mut raw_packet = [0; 16384]; + let (packet_length, _src_addr) = socket.recv_from(&mut raw_packet).expect("Drring, drring."); + let mut packet = BytesMut::with_capacity(16384); + packet.put_slice(&raw_packet[5..packet_length]); + return packet; + } + + pub fn send_handshake(socket: &UdpSocket, session_id: u32, addr: SocketAddr) { + send_packet(socket, addr, 9, session_id, &[0 as u8; 0]); + } + + pub fn recv_handshake(socket: &UdpSocket) -> u32 { + str::from_utf8(&recv_packet(socket)).expect("Non-UTF8 string.").trim_end_matches(char::from(0)).parse::().expect("Non-number string.") + } + + pub fn send_basicstat(socket: &UdpSocket, session_id: u32, addr: SocketAddr, challenge: u32) { + let mut challenge_token = BytesMut::with_capacity(32); + challenge_token.put_u32(challenge); + send_packet(socket, addr, 0, session_id, &challenge_token); + } + + pub fn recv_basicstat(socket: &UdpSocket) -> BasicStat { + let bs_buffer = &recv_packet(socket)[..]; + let bs_vector: Vec<&[u8]> = bs_buffer.split(|&ch| ch == 0).collect::>(); + BasicStat { + motd: slice_to_string(bs_vector[0]), + gametype: slice_to_string(bs_vector[1]), + map: slice_to_string(bs_vector[2]), + numplayers: str::from_utf8(bs_vector[3]).expect("Players are not UTF-8.").parse().expect("Not a number."), + maxplayers: str::from_utf8(bs_vector[4]).expect("Players are not UTF-8.").parse().expect("Not a number."), + hostport: LittleEndian::read_u16(&bs_vector[5][..2]), + hostip: slice_to_string(&bs_vector[5][2..]), + } + } + + pub fn send_fullstat(socket: &UdpSocket, session_id: u32, addr: SocketAddr, challenge: u32) { + let mut challenge_token = BytesMut::with_capacity(64); + challenge_token.put_u32(challenge); + challenge_token.put_u32(0); + send_packet(socket, addr, 0, session_id, &challenge_token); + } + + pub fn recv_fullstat(socket: &UdpSocket) -> FullStat { + let fs_buffer = &recv_packet(socket)[11..]; + let mut fs_vector: Vec<&[u8]> = fs_buffer.split(|&ch| ch == 0).collect::>(); + let mut fullset_kv: HashMap = HashMap::new(); + let mut players: Vec = Vec::new(); + while fs_vector[0] != [0;0] { + let key = fs_vector.remove(0); + let value = fs_vector.remove(0); + fullset_kv.insert(slice_to_string(key), slice_to_string(value)); + } + fs_vector.remove(0); + fs_vector.remove(0); // padding moment + fs_vector.remove(0); + while fs_vector[0] != [0;0] { + players.push(slice_to_string(fs_vector.remove(0))); + } + let mut fullstat = convert_kv(fullset_kv); + fullstat.set_players(players); + return fullstat; + } + +} + +fn usage(program: &str, opts: Options) { + let brief = format!("Usage: {} ADDRESS[:PORT]", program); + print!("{}", opts.usage(&brief)); +} + +fn main() { + let args: Vec = env::args().collect(); + let program = args[0].clone(); + let mut opts = Options::new(); + opts.optflag("h", "help", "display this help and exit"); + opts.optflag("b", "basicstats", "sends a basic stat request"); + let flags = match opts.parse(&args[1..]) { + Ok(m) => { m } + Err(f) => { panic!("{}", f.to_string()) } + }; + if flags.opt_present("h") { + usage(&program, opts); + return; + } + let basic_stat = flags.opt_present("b"); + let server = if !flags.free.is_empty() { + if flags.free[0].contains(":") { + flags.free[0].clone() + } else { + flags.free[0].clone() + ":25565" + } + } else { + usage(&program, opts); + return; + }; + + let mut rng = rand::thread_rng(); + let session_id = rng.gen::() & 0x0F0F0F0F; + + let addrs: Vec = server.to_socket_addrs().expect("Unable to resolve domain.").collect(); + let socket = UdpSocket::bind((Ipv4Addr::UNSPECIFIED, 0)).expect("Couldn't bind to address."); + let addr = addrs[0]; + + querynet::send_handshake(&socket, session_id, addr); + let challenge_token = querynet::recv_handshake(&socket); + + if basic_stat { + querynet::send_basicstat(&socket, session_id, addr, challenge_token); + let basic_stats = querynet::recv_fullstat(&socket); + println!("{}", basic_stats.json()); + } else { + querynet::send_fullstat(&socket, session_id, addr, challenge_token); + let full_stats = querynet::recv_fullstat(&socket); + println!("{}", full_stats.json()); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + static REQUEST_HS: [u8; 7] = [0xFE, 0xFD, 0x09, 0x00, 0x00, 0x0A, 0xFE]; + static RESPONSE_HS: [u8; 13] = [0x09, 0x00, 0x00, 0x0A, 0xFE, 0x39, 0x35, 0x31, 0x33, 0x33, 0x30, 0x37, 0x00]; + + // #[test] + // fn test_encode() { + // assert_eq!(&querynet::encode_packet(9, 2814, &[0 as u8;0])[..], REQUEST_HS); + // } + + // #[test] + // fn test_decode() { + // let packet = &RESPONSE_HS[..]; + // assert_eq!(querynet::decode_packet(packet), b"\x39\x35\x31\x33\x33\x30\x37\x00"); + // } + + #[test] + fn test_send_hs() { + let addr = SocketAddr::from(([127, 0, 0, 1], 5005)); + let socket = UdpSocket::bind(addr).expect("Couldn't bind to address."); + querynet::send_handshake(&socket, 2814, addr); + let mut handshake: [u8; 7] = [0 as u8 ;7]; + socket.recv_from(&mut handshake).expect("Didn't receive data."); + assert_eq!(handshake, REQUEST_HS); + } + + #[test] + fn test_receive_hs() { + let addr = SocketAddr::from(([127, 0, 0, 1], 5006)); + let socket = UdpSocket::bind(addr).expect("Couldn't bind to address."); + socket.send_to(&RESPONSE_HS[..], addr).expect("Didn't send"); + assert_eq!(querynet::recv_handshake(&socket), 9513307); + } + + #[test] + fn test_hs() { + let addr = SocketAddr::from(([51, 75, 186, 103], 25625)); // tarkoza lmao + let socket = UdpSocket::bind((Ipv4Addr::UNSPECIFIED, 0)).expect("Couldn't bind to address."); + let mut rng = rand::thread_rng(); + let session_id = rng.gen::() & 0x0F0F0F0F; + println!("SESSION_ID: {} ", session_id); + querynet::send_handshake(&socket, session_id, addr); + let ch_tk = querynet::recv_handshake(&socket); + println!("CHALLENGE_TOKEN: {}", ch_tk); + } + + #[test] + fn test_basicstat() { + let addr = SocketAddr::from(([51, 75, 186, 103], 25625)); // tarkoza lmao + let socket = UdpSocket::bind((Ipv4Addr::UNSPECIFIED, 0)).expect("Couldn't bind to address."); + let mut rng = rand::thread_rng(); + let session_id = rng.gen::() & 0x0F0F0F0F; + querynet::send_handshake(&socket, session_id, addr); + let challenge_token = querynet::recv_handshake(&socket); + querynet::send_basicstat(&socket, session_id, addr, challenge_token); + let basic_stats = querynet::recv_basicstat(&socket); + assert_eq!(basic_stats.map, "Tkz"); + } + + #[test] + fn test_fullstat() { + let addr = SocketAddr::from(([51, 75, 186, 103], 25625)); // tarkoza lmao + let socket = UdpSocket::bind((Ipv4Addr::UNSPECIFIED, 0)).expect("Couldn't bind to address."); + let mut rng = rand::thread_rng(); + let session_id = rng.gen::() & 0x0F0F0F0F; + querynet::send_handshake(&socket, session_id, addr); + let challenge_token = querynet::recv_handshake(&socket); + querynet::send_fullstat(&socket, session_id, addr, challenge_token); + let full_stats = querynet::recv_fullstat(&socket); + assert_eq!(full_stats.map, "Tkz"); + } +} -- cgit v1.2.3