commit a379a140d86134ed52b8f226e2f28e374ef9a124 Author: EliasSchriefer Date: Fri Jan 8 19:42:24 2021 +0100 Very late initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..96ef6c0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/target +Cargo.lock diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..9e19825 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "mcquery" +version = "0.1.0" +description = "Query Minecraft servers with Rust" +authors = ["EliasSchriefer "] +license = "MIT OR Apache-2.0" +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies.tokio] +version = "1.0" +features = ["net", "time"] + +[dev-dependencies.tokio] +version = "1.0" +features = ["macros", "net", "rt-multi-thread", "time"] \ No newline at end of file diff --git a/LICENSE-APACHE b/LICENSE-APACHE new file mode 100644 index 0000000..fce62cc --- /dev/null +++ b/LICENSE-APACHE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2021 Elias Schriefer + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/LICENSE-MIT b/LICENSE-MIT new file mode 100644 index 0000000..097d299 --- /dev/null +++ b/LICENSE-MIT @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 Elias Schriefer + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..4aca003 --- /dev/null +++ b/README.md @@ -0,0 +1,57 @@ +# `mcquery` +A tool for querying Minecraft servers + +## What does this crate provide? + - An asynchronous, [`tokio`](https://tokio.rs/)-based library + - Two examples (basic and full queries) + +## Usage +To perform a query, call `Query::get_basic` or `Query::get_full`. The +returned response will either be a `BasicQueryResponse` or a `FullQueryResponse` +respectively. + +## Examples +```rust +use mcquery::Query; +use std::time::Duration; + +#[tokio::main] +async fn main() -> std::io::Result<()> { + let mut basic_query_buffer = [0; 128]; + let basic_query = Query::get_basic( + "example.com:25565", + Duration::from_secs(5), + &mut basic_buffer + ).await?; + + println!("{:#?}", basic_query); + + let mut full_query_buffer = [0; 1024]; + let full_query = Query::get_full( + "example.com:25565", + Duration::from_secs(5), + &mut buffer + ).await?; + + println!("{:#?}", full_query); + + Ok(()) +} +``` + +## Participation +Feel free to help resolve issues, create new ones, or open a pull request. + +## What's next? + - Synchronous queries for those who want them + - A `Packet` trait (currently async trait functions are not supported by Rust) + or `Request`/`Response` structs + +## Development resources +Thank you to the [Tokio project](https://tokio.rs/) and the [Minecraft Developer +Wiki](https://wiki.vg/) +for providing a great asynchronous runtime and useful documentation! Without them, +this crate wouldn't be possible. + +## License +This project is either licensed under the MIT or the Apache 2.0 license. \ No newline at end of file diff --git a/examples/basic_query.rs b/examples/basic_query.rs new file mode 100644 index 0000000..ddc1706 --- /dev/null +++ b/examples/basic_query.rs @@ -0,0 +1,59 @@ +use mcquery::Query; +use std::{ + time::Duration, + env +}; + +#[tokio::main] +async fn main() { + let buffer_size = match env::args().nth(3) { + Some(arg) => match arg.parse() { + Ok(size) => size, + Err(err) => { + eprintln!("Error parsing buffer size"); + eprintln!("{}", err); + std::process::exit(1); + }, + }, + None => 128, + }; + + println!("Creating {}B buffer...", buffer_size); + let mut buffer = vec![0; buffer_size]; + + let address = ( + match env::args().nth(1) { + Some(ip) => ip, + None => { + eprintln!("No server IP specified"); + std::process::exit(1); + }, + }, match env::args().nth(2).unwrap_or("25565".to_owned()).parse() { + Ok(port) => port, + Err(err) => { + eprintln!("Error parsing port"); + eprintln!("{}", err); + std::process::exit(1); + }, + } + ); + println!("Connecting with {}:{}...", address.0, address.1); + + if let Query::Basic(stat) = match Query::get_basic( + address, + Duration::from_secs(5), + &mut buffer + ).await { + Ok(query) => query, + Err(err) => { + eprintln!("{}", err); + std::process::exit(1); + } + } { + println!("\nBasic query response:"); + println!(" MOTD: {}", stat.motd); + println!(" Map: {}", stat.map); + println!(" Players: {}/{}", stat.num_players, stat.max_players); + println!(" Host: {}:{}", stat.ip, stat.port); + }; +} \ No newline at end of file diff --git a/examples/full_query.rs b/examples/full_query.rs new file mode 100644 index 0000000..1c76bf4 --- /dev/null +++ b/examples/full_query.rs @@ -0,0 +1,78 @@ +use mcquery::Query; +use std::{ + time::Duration, + env +}; + +#[tokio::main] +async fn main() { + let buffer_size = match env::args().nth(3) { + Some(arg) => match arg.parse() { + Ok(size) => size, + Err(err) => { + eprintln!("Error parsing buffer size"); + eprintln!("{}", err); + std::process::exit(1); + }, + }, + None => 1024, + }; + + println!("Creating {}B buffer...", buffer_size); + let mut buffer = vec![0; buffer_size]; + + let address = ( + match env::args().nth(1) { + Some(ip) => ip, + None => { + eprintln!("No server IP specified"); + std::process::exit(1); + }, + }, match env::args().nth(2).unwrap_or("25565".to_owned()).parse() { + Ok(port) => port, + Err(err) => { + eprintln!("Error parsing port"); + eprintln!("{}", err); + std::process::exit(1); + }, + } + ); + println!("Connecting with {}:{}...", address.0, address.1); + + if let Query::Full(stat) = match Query::get_full( + address, + Duration::from_secs(5), + &mut buffer + ).await { + Ok(query) => query, + Err(err) => { + eprintln!("{}", err); + std::process::exit(1); + } + } { + println!("\nFull query response:"); + println!(" KV section: {{"); + for (key, value) in stat.kv { + println!(" {}: {}", key, value); + } + println!(" }}"); + println!(" Players: {}", + if stat.players.len() > 0 { + stat.players.join(", ") + } else { + "–".to_owned() + } + ); + println!(" Software: {}", stat.software.unwrap_or("vanilla".to_owned())); + print!(" Plugins:"); + match stat.plugins { + Some(plugins) => { + println!(); + for plugin in plugins { + println!(" {}", plugin); + } + }, + None => println!(" –") + }; + }; +} \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..6dc12ee --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,507 @@ +#![deny(missing_docs)] +//! This library provides basically two functions for querying Minecraft +//! servers. For increased stability, an extra function for handshakes +//! is not provided. +//! +//! ## Usage +//! To perform a query, call [`Query::get_basic`] or [`Query::get_full`]. +//! The returned response will either be a [`BasicQueryResponse`] or a +//! [`FullQueryResponse`] respectively. +//! +//! ## Development resources +//! Thank you to the [Tokio project] and the [Minecraft Developer Wiki] for +//! providing a great asynchronous runtime and useful documentation! +//! +//! Without them, this crate wouldn't be possible. +//! +//! [Tokio project]: https://tokio.rs/ +//! [Minecraft Developer Wiki]: https://wiki.vg/ + +use std::{ + collections::HashMap, + io, + time::Duration +}; +use tokio::{ + net::{ ToSocketAddrs, UdpSocket }, + time::timeout as tokioTimeout, +}; + +/// Bitmask to validate a session ID +/// +/// Usually you don't need this. +/// +/// # Example +/// ``` +/// use mcquery::SESSION_ID_MASK; +/// +/// # #[allow(unused_variables)] +/// # fn main() { +/// let session_id = 0x12345678_u32; +/// let valid_session_id = session_id & SESSION_ID_MASK; +/// # } +/// ``` +pub const SESSION_ID_MASK: u32 = 0x0F0F0F0F; +const REQUEST_HEADER: [u8; 2] = [0xFE, 0xFD]; + +static mut SESSION_ID_COUNTER: u16 = 0; + +fn gen_session_id() -> u32 { + unsafe { + SESSION_ID_COUNTER = SESSION_ID_COUNTER.wrapping_add(1); + } + + let mut session_id_bytes = [0; 4]; + for (i, b) in unsafe { SESSION_ID_COUNTER }.to_be_bytes().iter().enumerate() { + session_id_bytes[i * 2] = b >> 4; + session_id_bytes[i * 2 + 1] = b & 0x0F; + } + u32::from_be_bytes(session_id_bytes) +} + +/// Encountered when the buffer is full. +/// +/// # Desciption +/// The response may be incomplete, if the buffer has been filled up, +/// because excess bytes that don't fit into it are discarded. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub struct MaybeIncompleteDataError(usize); + +impl std::error::Error for MaybeIncompleteDataError {} + +impl core::fmt::Display for MaybeIncompleteDataError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "Buffer filled up") + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +enum PacketType { + Handshake = 0x09, + Query = 0x00, +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] +struct HandshakeRequest { + session_id: u32, +} + +impl HandshakeRequest { + const PACKET_TYPE: PacketType = PacketType::Handshake; + + fn bytes(&self) -> Vec { + let mut vec = Vec::new(); + vec.extend_from_slice(&REQUEST_HEADER); + vec.push(Self::PACKET_TYPE as u8); + vec.extend_from_slice(&self.session_id.to_be_bytes()); + + vec + } + + async fn send(&self, socket: &UdpSocket, timeout: Duration) -> io::Result { + // Sending + tokioTimeout(timeout, socket.send(self.bytes().as_slice())).await??; + + // Receiving + let mut response_buffer = [0; 16]; + let bytes_read = tokioTimeout(timeout, socket.recv(&mut response_buffer)).await??; + + // Parsing + let mut response_buffer = response_buffer[..bytes_read].to_owned(); + let res_packet_type = response_buffer.remove(0); + let mut res_session_id = [0; 4]; + + for (i, b) in response_buffer.drain(..4).enumerate() { + res_session_id[i] = b; + } + + let res_session_id = u32::from_be_bytes(res_session_id); + let res_challenge_token = + String::from_utf8( + response_buffer + .into_iter() + .take_while(|x| x != &0x00) + .collect() + ).map_err(|_| io::ErrorKind::InvalidData)? + .parse() + .map_err(|_| io::ErrorKind::InvalidData)?; + + if res_packet_type != Self::PACKET_TYPE as u8 || res_session_id != self.session_id { + Err(io::ErrorKind::InvalidData.into()) + } else { + Ok(HandshakeResponse { + session_id: res_session_id, + challenge_token: res_challenge_token, + }) + } + } +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] +struct HandshakeResponse { + session_id: u32, + challenge_token: i32, +} + +impl From for BasicQueryRequest { + fn from(res: HandshakeResponse) -> Self { + BasicQueryRequest { + session_id: res.session_id, + challenge_token: res.challenge_token, + } + } +} + +impl From for FullQueryRequest { + fn from(res: HandshakeResponse) -> Self { + FullQueryRequest { + session_id: res.session_id, + challenge_token: res.challenge_token, + } + } +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] +struct BasicQueryRequest { + session_id: u32, + challenge_token: i32, +} + +impl BasicQueryRequest { + const PACKET_TYPE: PacketType = PacketType::Query; + + fn bytes(&self) -> Vec { + let mut vec = Vec::new(); + vec.extend_from_slice(&REQUEST_HEADER); + vec.push(Self::PACKET_TYPE as u8); + vec.extend_from_slice(&self.session_id.to_be_bytes()); + vec.extend_from_slice(&self.challenge_token.to_be_bytes()); + + vec + } + + async fn send(&self, socket: &UdpSocket, timeout: Duration, buffer: &mut [u8]) -> io::Result { + // Sending + tokioTimeout(timeout, socket.send(self.bytes().as_slice())).await??; + + // Receiving + let bytes_read = tokioTimeout(timeout, socket.recv(buffer)).await??; + if bytes_read == buffer.len() { + return Err(io::Error::new(io::ErrorKind::Other, MaybeIncompleteDataError(bytes_read))); + } + + // Parsing + let mut response_buffer: String = buffer[0..bytes_read].iter().map(|b| *b as char).collect(); + + // Packet type + let res_packet_type = response_buffer.remove(0) as u8; + + // Session ID + let mut res_session_id = [0; 4]; + for (i, b) in response_buffer.drain(..4).enumerate() { + res_session_id[i] = b as u8; + } + let res_session_id = u32::from_be_bytes(res_session_id); + + let ( + res_motd, + res_gametype, + res_map, + res_num_players, + res_max_players, + mut res_address + ) = { + let mut i = 0; + let vec: Vec<_> = response_buffer + .split_terminator(|c| { + if i < 5 { + if c == '\0' { + i += 1; + } + c == '\0' + } else { + i += 1; + i > 7 && c == '\0' + } + }) + .collect(); + ( + vec[0].clone(), + vec[1].clone(), + vec[2].clone(), + vec[3].clone(), + vec[4].clone(), + vec[5].chars().map(|c| c as u8).collect::>(), + ) + }; + + // Number of players + let res_num_players = res_num_players.parse().map_err(|_| io::ErrorKind::InvalidData)?; + + // Max number of players + let res_max_players = res_max_players.parse().map_err(|_| io::ErrorKind::InvalidData)?; + + // Host port + let mut res_port = [0; 2]; + for (i, b) in res_address.drain(..2).enumerate() { + res_port[i] = b; + } + let res_port = u16::from_le_bytes(res_port); + + // Host IP + let res_ip = String::from_utf8(res_address).map_err(|_| io::ErrorKind::InvalidData)?; + + if res_packet_type != Self::PACKET_TYPE as u8 || res_session_id != self.session_id { + Err(io::ErrorKind::InvalidData.into()) + } else { + Ok(BasicQueryResponse { + session_id: res_session_id, + motd: res_motd.to_owned(), + gametype: res_gametype.to_owned(), + map: res_map.to_owned(), + num_players: res_num_players, + max_players: res_max_players, + port: res_port, + ip: res_ip, + }) + } + } +} + +/// Response to a basic query +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] +pub struct BasicQueryResponse { + session_id: u32, + /// The Motto of the Day + pub motd: String, + /// Should always be `"SMP"` + pub gametype: String, + /// The default map + pub map: String, + /// Number of players currently online + pub num_players: u32, + /// Maximum player capacity + pub max_players: u32, + /// The host port. Default is `25565` + pub port: u16, + /// The host ip. This is the localhost by default + pub ip: String, +} + +struct FullQueryRequest { + session_id: u32, + challenge_token: i32, +} + +impl FullQueryRequest { + const PACKET_TYPE: PacketType = PacketType::Query; + + fn bytes(&self) -> Vec { + let mut vec = Vec::new(); + vec.extend_from_slice(&REQUEST_HEADER); + vec.push(Self::PACKET_TYPE as u8); + vec.extend_from_slice(&self.session_id.to_be_bytes()); + vec.extend_from_slice(&self.challenge_token.to_be_bytes()); + vec.extend_from_slice(&[0; 4]); + + vec + } + + async fn send(&self, socket: &UdpSocket, timeout: Duration, buffer: &mut [u8]) -> io::Result { + // Sending + tokioTimeout(timeout, socket.send(self.bytes().as_slice())).await??; + + // Receiving + let bytes_read = tokioTimeout(timeout, socket.recv(buffer)).await??; + if bytes_read == buffer.len() { + return Err(io::Error::new(io::ErrorKind::Other, MaybeIncompleteDataError(bytes_read))); + } + + // Parsing + let mut response_buffer = buffer[0..bytes_read].to_vec(); + + // Packet type + let res_packet_type = response_buffer.remove(0); + + // Session ID + let mut res_session_id = [0; 4]; + for (i, b) in response_buffer.drain(..4).enumerate() { + res_session_id[i] = b + } + let res_session_id = u32::from_be_bytes(res_session_id); + + // Key value section + let mut res_kv = HashMap::::new(); + loop { + let key: Vec = response_buffer.iter() + .take_while(|b| **b != 0x00) + .map(|&b| b) + .collect(); + response_buffer.drain(0..key.len() + 1); + if key.len() == 0 { + break; + } + + let value: Vec = response_buffer.iter() + .take_while(|b| **b != 0x00) + .map(|&b| b) + .collect(); + response_buffer.drain(0..value.len() + 1); + + res_kv.insert( + key.iter() + .map(|b| *b as char) + .collect(), + value.iter() + .map(|b| *b as char) + .collect() + ); + } + res_kv.remove("splitnum").ok_or(io::ErrorKind::InvalidData)?; + + // Plugins and server software + let ( + res_software, + res_plugins + ) = if let Some(mut plugins) = res_kv.get("plugins") + .map(|s| s.clone()) + { + res_kv.remove("plugins"); + // let mut plugin_string = plugin_string; + let software: String = plugins.chars().take_while(|c| *c != ':').collect(); + plugins.drain(0..software.len() + 1); + + let plugins = if plugins.len() > 0 { + Some(plugins.split(';').map(|s| s.trim().to_owned()).collect()) + } else { + None + }; + + (Some(software), plugins) + } else { + (None, None) + }; + + // Players + { + let player_header = "\x01player_\0\0"; + if !response_buffer.starts_with(player_header.as_bytes()) { + return Err(io::ErrorKind::InvalidData.into()); + } else { + response_buffer.drain(0..player_header.len()); + } + } + let mut res_players = Vec::new(); + loop { + let player: Vec = response_buffer.iter() + .take_while(|b| **b != 0x00) + .map(|&b| b) + .collect(); + response_buffer.drain(0..player.len()); + if player.len() == 0 { + break; + } + + res_players.push( + player.iter() + .map(|b| *b as char) + .collect() + ); + } + + if res_packet_type != Self::PACKET_TYPE as u8 || res_session_id != self.session_id { + Err(io::ErrorKind::InvalidData.into()) + } else { + Ok(FullQueryResponse { + session_id: res_session_id, + kv: res_kv, + players: res_players, + software: res_software, + plugins: res_plugins, + }) + } + } +} + +/// Response to a full query +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct FullQueryResponse { + session_id: u32, + /// Additional server information + pub kv: HashMap, + /// Names of players currently online + pub players: Vec, + /// The software string. Empty if vanilla server or not supported + pub software: Option, + /// Plugins used on the server. Empty if vanilla server or not supported + pub plugins: Option> +} + +/// The main data structure in this crate +/// +/// # Description +/// Basic queries are enabled by default. They can be diabled in the server's +/// `server.properties` with `enable-status=true`. This is what is used in the +/// server list inside Minecraft. +/// +/// Full queries are diabled by default. They can be enabled in the server's +/// `server.properties` with `enable-query=true`. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Query { + /// Response to a basic query. Can be obtained with [`Query::get_basic()`] + /// + /// See [`BasicQueryResponse`] for more + Basic(BasicQueryResponse), + /// Response to a full query. Can be obtained with [`Query::get_full()`] + /// + /// See [`FullQueryResponse`] for more + Full(FullQueryResponse), +} + +impl Query { + /// Connects with the Minecraft server at `address` and performs a basic query. + /// `timeout` is used as a response timeout. The specified buffer needs to be big + /// enough to hold all response bytes, else this function will return a + /// [`MaybeIncompleteDataError`] wrapped in an [`std::io::Error`]. The [`Ok`] value + /// will be [`Query::Basic`]. + pub async fn get_basic(address: A, timeout: Duration, buffer: &mut [u8]) -> io::Result { + let socket = UdpSocket::bind("0.0.0.0:0").await?; + socket.connect(address).await?; + + let handshake_req = HandshakeRequest { + session_id: gen_session_id(), + }; + + let handshake_res = handshake_req.send(&socket, timeout).await?; + + let query_req: BasicQueryRequest = handshake_res.into(); + + buffer.iter_mut().for_each(|b| *b = 0); + let query_res = query_req.send(&socket, timeout, buffer).await?; + + Ok(Query::Basic(query_res)) + } + + /// Connects with the Minecraft server at `address` and performs a full query. + /// `timeout` is used as a response timeout. The specified buffer needs to be big + /// enough to hold all response bytes, else this function will return a + /// [`MaybeIncompleteDataError`] wrapped in an [`std::io::Error`]. The [`Ok`] vlaue + /// will be [`Query::Full`]. + pub async fn get_full(address: A, timeout: Duration, buffer: &mut [u8]) -> io::Result { + let socket = UdpSocket::bind("0.0.0.0:0").await?; + socket.connect(address).await?; + + let handshake_req = HandshakeRequest { + session_id: gen_session_id(), + }; + + let handshake_res = handshake_req.send(&socket, timeout).await?; + + let query_req: FullQueryRequest = handshake_res.into(); + + buffer.iter_mut().for_each(|b| *b = 0); + let query_res = query_req.send(&socket, timeout, buffer).await?; + + Ok(Query::Full(query_res)) + } +} \ No newline at end of file