Minecraft implementation (#6)

* Initial minecraft support

* Made previews_chat an option

* Better error handling and removed version structure

* Minecraft Server types

* Fixed compilation and renamed stuff

* 'extract till you drop!' extracted sockets

* extracted java version and fixed socket udp receive

* Legacy 1.4 and 1.6 implementation (incomplete)

* Furter implementation

* Implementations work

* Protocol beta v1.8+ implemented

* Removed bedrock support

* Added auto query

* Renamed minecraft to mc and added to md's

* Docs, renames and small optimization changes

* Changed java version to be able to return None on players sample
This commit is contained in:
CosminPerRam 2022-11-24 22:52:54 +02:00 committed by GitHub
parent 974e093e23
commit ee0223a7a3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 810 additions and 80 deletions

View file

@ -1,6 +1,9 @@
Who knows what the future holds...
# 0.0.6 - ??/??/2022
[Minecraft](https://www.minecraft.com) implementation (bedrock not supported yet).
# 0.0.5 - 15/11/2022
Added `SocketBind` error, regarding failing to bind a socket.
Socket custom timeout capability (with an error if provided durations are zero).

View file

@ -15,6 +15,9 @@ keywords = ["server", "verify", "game", "check", "status"]
msrv = "1.58.1"
[dependencies]
bzip2-rs = "0.1.2" # for compression
crc32fast = "1.3.2"
bzip2-rs = "0.1.2"
trust-dns-resolver = "0.22.0"
trust-dns-resolver = "0.22.0" # dns resolving
serde_json = "1.0.87" # json to structs

View file

@ -17,7 +17,7 @@
| INSMIC | Insurgency: Modern Infantry Combat | Valve Protocol | Not tested. |
| CSCZ | Counter-Strike: Condition Zero | Valve Protocol | |
| DOD | Day of Defeat | Valve Protocol | |
| MC | Minecraft | Proprietary | Bedrock not supported yet. |
## Planned to add support:
All Valve titles.
Minecraft.
_

View file

@ -3,6 +3,7 @@
| Name | Documentation reference | Notes |
|----------------|---------------------------------------------------------------------------|----------------------------------------|
| Valve Protocol | [Server Queries](https://developer.valvesoftware.com/wiki/Server_queries) | Multi-packet decompression not tested. |
| Minecraft | [List Server Protocol](https://wiki.vg/Server_List_Ping) | Bedrock not yet supported. |
## Planned to add support:
Minecraft protocol
_

View file

@ -1,6 +1,7 @@
use std::env;
use gamedig::{aliens, asrd, cscz, csgo, css, dod, dods, GDResult, gm, hl2dm, ins, insmic, inss, l4d, l4d2, tf2, ts};
use gamedig::{aliens, asrd, cscz, csgo, css, dod, dods, GDResult, gm, hl2dm, ins, insmic, inss, l4d, l4d2, mc, tf2, ts};
use gamedig::protocols::minecraft::{LegacyGroup, Server};
use gamedig::protocols::valve;
use gamedig::protocols::valve::App;
@ -41,6 +42,11 @@ fn main() -> GDResult<()> {
"ts" => println!("{:?}", ts::query(ip, port)?),
"cscz" => println!("{:?}", cscz::query(ip, port)?),
"dod" => println!("{:?}", dod::query(ip, port)?),
"mc" => println!("{:?}", mc::query(ip, port)?),
"mc_java" => println!("{:?}", mc::query_specific(Server::Java, ip, port)?),
"mc_legacy_v1_4" => println!("{:?}", mc::query_specific(Server::Legacy(LegacyGroup::V1_4), ip, port)?),
"mc_legacy_v1_6" => println!("{:?}", mc::query_specific(Server::Legacy(LegacyGroup::V1_6), ip, port)?),
"mc_legacy_vb1_8" => println!("{:?}", mc::query_specific(Server::Legacy(LegacyGroup::VB1_8), ip, port)?),
"_src" => println!("{:?}", valve::query(ip, 27015, App::Source(None), None, None)?),
"_gld" => println!("{:?}", valve::query(ip, 27015, App::GoldSrc(false), None, None)?),
"_gld_f" => println!("{:?}", valve::query(ip, 27015, App::GoldSrc(true), None, None)?),

10
examples/minecraft.rs Normal file
View file

@ -0,0 +1,10 @@
use gamedig::games::mc;
fn main() {
let response = mc::query("localhost", None); //or Some(25565), None is the default protocol port (which is 25565)
match response {
Err(error) => println!("Couldn't query, error: {error}"),
Ok(r) => println!("{:?}", r)
}
}

View file

@ -32,6 +32,12 @@ pub enum GDError {
SocketBind(String),
/// Invalid input.
InvalidInput(String),
/// Couldn't create a socket connection.
SocketConnect(String),
/// Couldn't parse a json string.
JsonParse(String),
/// Couldn't parse a json string.
AutoQuery(String),
}
impl fmt::Display for GDError {
@ -48,6 +54,9 @@ impl fmt::Display for GDError {
GDError::DnsResolve(details) => write!(f, "DNS Resolve: {details}"),
GDError::SocketBind(details) => write!(f, "Socket bind: {details}"),
GDError::InvalidInput(details) => write!(f, "Invalid input: {details}"),
GDError::SocketConnect(details) => write!(f, "Socket connect: {details}"),
GDError::JsonParse(details) => write!(f, "Json parse: {details}"),
GDError::AutoQuery(details) => write!(f, "Auto query: {details}"),
}
}
}

18
src/games/mc.rs Normal file
View file

@ -0,0 +1,18 @@
use crate::GDResult;
use crate::protocols::minecraft;
use crate::protocols::minecraft::{Server, Response};
pub fn query(address: &str, port: Option<u16>) -> GDResult<Response> {
minecraft::query(address, port_or_default(port), None)
}
pub fn query_specific(mc_type: Server, address: &str, port: Option<u16>) -> GDResult<Response> {
minecraft::query_specific(mc_type, address, port_or_default(port), None)
}
fn port_or_default(port: Option<u16>) -> u16 {
match port {
None => 25565,
Some(port) => port
}
}

View file

@ -33,3 +33,5 @@ pub mod insmic;
pub mod cscz;
/// Day of Defeat
pub mod dod;
/// Minecraft
pub mod mc;

View file

@ -15,10 +15,13 @@
//! }
//! ```
extern crate core;
pub mod errors;
pub mod protocols;
pub mod games;
mod utils;
mod socket;
pub use errors::*;
pub use games::*;

View file

@ -0,0 +1,9 @@
/// The implementation.
pub mod protocol;
/// All types used by the implementation.
pub mod types;
pub use protocol::*;
pub use types::*;
pub use protocol::*;

View file

@ -0,0 +1,127 @@
use serde_json::Value;
use crate::{GDError, GDResult};
use crate::protocols::minecraft::{as_varint, get_string, get_varint, Player, Response, Server};
use crate::protocols::types::TimeoutSettings;
use crate::socket::{Socket, TcpSocket};
pub struct Java {
socket: TcpSocket
}
impl Java {
fn new(address: &str, port: u16, timeout_settings: Option<TimeoutSettings>) -> GDResult<Self> {
let socket = TcpSocket::new(address, port)?;
socket.apply_timeout(timeout_settings)?;
Ok(Self {
socket
})
}
fn send(&mut self, data: Vec<u8>) -> GDResult<()> {
self.socket.send(&[as_varint(data.len() as i32), data].concat())
}
fn receive(&mut self) -> GDResult<Vec<u8>> {
let buf = self.socket.receive(None)?;
let mut pos = 0;
let _packet_length = get_varint(&buf, &mut pos)? as usize;
//this declared 'packet length' from within the packet might be wrong (?), not checking with it...
Ok(buf[pos..].to_vec())
}
fn send_handshake(&mut self) -> GDResult<()> {
self.send([
//Packet ID (0)
0x00,
//Protocol Version (-1 to determine version)
0xFF, 0xFF, 0xFF, 0xFF, 0x0F,
//Server address (can be anything)
0x07, 0x47, 0x61, 0x6D, 0x65, 0x44, 0x69, 0x67,
//Server port (can be anything)
0x00, 0x00,
//Next state (1 for status)
0x01].to_vec())?;
Ok(())
}
fn send_status_request(&mut self) -> GDResult<()> {
self.send([
//Packet ID (0)
0x00].to_vec())?;
Ok(())
}
fn send_ping_request(&mut self) -> GDResult<()> {
self.send([
//Packet ID (1)
0x01].to_vec())?;
Ok(())
}
fn get_info(&mut self) -> GDResult<Response> {
self.send_handshake()?;
self.send_status_request()?;
self.send_ping_request()?;
let buf = self.receive()?;
let mut pos = 0;
if get_varint(&buf, &mut pos)? != 0 { //first var int is the packet id
return Err(GDError::PacketBad("Bad receive packet id.".to_string()));
}
let json_response = get_string(&buf, &mut pos)?;
let value_response: Value = serde_json::from_str(&json_response)
.map_err(|e| GDError::JsonParse(e.to_string()))?;
let version_name = value_response["version"]["name"].as_str()
.ok_or(GDError::PacketBad("Couldn't get expected string.".to_string()))?.to_string();
let version_protocol = value_response["version"]["protocol"].as_i64()
.ok_or(GDError::PacketBad("Couldn't get expected number.".to_string()))? as i32;
let max_players = value_response["players"]["max"].as_u64()
.ok_or(GDError::PacketBad("Couldn't get expected number.".to_string()))? as u32;
let online_players = value_response["players"]["online"].as_u64()
.ok_or(GDError::PacketBad("Couldn't get expected number.".to_string()))? as u32;
let sample_players: Option<Vec<Player>> = match value_response["players"]["sample"].is_null() {
true => None,
false => Some({
let players_values = value_response["players"]["sample"].as_array()
.ok_or(GDError::PacketBad("Couldn't get expected array.".to_string()))?;
let mut players = Vec::with_capacity(players_values.len());
for player in players_values {
players.push(Player {
name: player["name"].as_str().ok_or(GDError::PacketBad("Couldn't get expected string.".to_string()))?.to_string(),
id: player["id"].as_str().ok_or(GDError::PacketBad("Couldn't get expected string.".to_string()))?.to_string()
})
}
players
})
};
Ok(Response {
version_name,
version_protocol,
max_players,
online_players,
sample_players,
description: value_response["description"].to_string(),
favicon: value_response["favicon"].as_str().map(str::to_string),
previews_chat: value_response["previewsChat"].as_bool(),
enforces_secure_chat: value_response["enforcesSecureChat"].as_bool(),
server_type: Server::Java
})
}
pub fn query(address: &str, port: u16, timeout_settings: Option<TimeoutSettings>) -> GDResult<Response> {
Java::new(address, port, timeout_settings)?.get_info()
}
}

View file

@ -0,0 +1,71 @@
use crate::{GDError, GDResult};
use crate::protocols::minecraft::{LegacyGroup, Response, Server};
use crate::protocols::types::TimeoutSettings;
use crate::socket::{Socket, TcpSocket};
use crate::utils::buffer::{get_string_utf16_be, get_u16_be, get_u8};
pub struct LegacyBV1_8 {
socket: TcpSocket
}
impl LegacyBV1_8 {
fn new(address: &str, port: u16, timeout_settings: Option<TimeoutSettings>) -> GDResult<Self> {
let socket = TcpSocket::new(address, port)?;
socket.apply_timeout(timeout_settings)?;
Ok(Self {
socket
})
}
fn send_initial_request(&mut self) -> GDResult<()> {
self.socket.send(&[0xFE])
}
fn get_info(&mut self) -> GDResult<Response> {
self.send_initial_request()?;
let buf = self.socket.receive(None)?;
let mut pos = 0;
if get_u8(&buf, &mut pos)? != 0xFF {
return Err(GDError::PacketBad("Expected 0xFF".to_string()));
}
let length = get_u16_be(&buf, &mut pos)? * 2;
if buf.len() != (length + 3) as usize { //+ 3 because of the first byte and the u16
return Err(GDError::PacketBad("Not right size".to_string()));
}
let packet_string = get_string_utf16_be(&buf, &mut pos)?;
let split: Vec<&str> = packet_string.split("§").collect();
if split.len() != 3 {
return Err(GDError::PacketBad("Not right size".to_string()));
}
let description = split[0].to_string();
let online_players = split[1].parse()
.map_err(|_| GDError::PacketBad("Expected int".to_string()))?;
let max_players = split[2].parse()
.map_err(|_| GDError::PacketBad("Expected int".to_string()))?;
Ok(Response {
version_name: "Beta 1.8+".to_string(),
version_protocol: -1,
max_players,
online_players,
sample_players: None,
description,
favicon: None,
previews_chat: None,
enforces_secure_chat: None,
server_type: Server::Legacy(LegacyGroup::VB1_8)
})
}
pub fn query(address: &str, port: u16, timeout_settings: Option<TimeoutSettings>) -> GDResult<Response> {
LegacyBV1_8::new(address, port, timeout_settings)?.get_info()
}
}

View file

@ -0,0 +1,76 @@
use crate::{GDError, GDResult};
use crate::protocols::minecraft::{LegacyGroup, Response, Server};
use crate::protocols::minecraft::protocol::legacy_v1_6::LegacyV1_6;
use crate::protocols::types::TimeoutSettings;
use crate::socket::{Socket, TcpSocket};
use crate::utils::buffer::{get_string_utf16_be, get_u16_be, get_u8};
pub struct LegacyV1_4 {
socket: TcpSocket
}
impl LegacyV1_4 {
fn new(address: &str, port: u16, timeout_settings: Option<TimeoutSettings>) -> GDResult<Self> {
let socket = TcpSocket::new(address, port)?;
socket.apply_timeout(timeout_settings)?;
Ok(Self {
socket
})
}
fn send_initial_request(&mut self) -> GDResult<()> {
self.socket.send(&[0xFE, 0x01])
}
fn get_info(&mut self) -> GDResult<Response> {
self.send_initial_request()?;
let buf = self.socket.receive(None)?;
let mut pos = 0;
if get_u8(&buf, &mut pos)? != 0xFF {
return Err(GDError::PacketBad("Expected 0xFF".to_string()));
}
let length = get_u16_be(&buf, &mut pos)? * 2;
if buf.len() != (length + 3) as usize { //+ 3 because of the first byte and the u16
return Err(GDError::PacketBad("Not right size".to_string()));
}
if LegacyV1_6::is_protocol(&buf, &mut pos)? {
return LegacyV1_6::get_response(&buf, &mut pos);
}
let packet_string = get_string_utf16_be(&buf, &mut pos)?;
let split: Vec<&str> = packet_string.split("§").collect();
if split.len() != 3 {
return Err(GDError::PacketBad("Not right size".to_string()));
}
let description = split[0].to_string();
let online_players = split[1].parse()
.map_err(|_| GDError::PacketBad("Expected int".to_string()))?;
let max_players = split[2].parse()
.map_err(|_| GDError::PacketBad("Expected int".to_string()))?;
Ok(Response {
version_name: "1.4+".to_string(),
version_protocol: -1,
max_players,
online_players,
sample_players: None,
description,
favicon: None,
previews_chat: None,
enforces_secure_chat: None,
server_type: Server::Legacy(LegacyGroup::V1_4)
})
}
pub fn query(address: &str, port: u16, timeout_settings: Option<TimeoutSettings>) -> GDResult<Response> {
LegacyV1_4::new(address, port, timeout_settings)?.get_info()
}
}

View file

@ -0,0 +1,103 @@
use crate::{GDError, GDResult};
use crate::protocols::minecraft::{LegacyGroup, Response, Server};
use crate::protocols::types::TimeoutSettings;
use crate::socket::{Socket, TcpSocket};
use crate::utils::buffer::{get_string_utf16_be, get_u16_be, get_u8};
pub struct LegacyV1_6 {
socket: TcpSocket
}
impl LegacyV1_6 {
fn new(address: &str, port: u16, timeout_settings: Option<TimeoutSettings>) -> GDResult<Self> {
let socket = TcpSocket::new(address, port)?;
socket.apply_timeout(timeout_settings)?;
Ok(Self {
socket
})
}
fn send_initial_request(&mut self) -> GDResult<()> {
self.socket.send(&[
// Packet ID (FE)
0xfe,
// Ping payload (01)
0x01,
// Packet identifier for plugin message
0xfa,
// Length of 'GameDig' string (7) as unsigned short
0x00, 0x07,
// 'GameDig' string as UTF-16BE
0x00, 0x47, 0x00, 0x61, 0x00, 0x6D, 0x00, 0x65, 0x00, 0x44, 0x00, 0x69, 0x00, 0x67])?;
Ok(())
}
pub fn is_protocol(buf: &[u8], pos: &mut usize) -> GDResult<bool> {
let state = buf[*pos..].starts_with(&[0x00, 0xA7, 0x00, 0x31, 0x00, 0x00]);
if state {
*pos += 6;
}
Ok(state)
}
pub fn get_response(buf: &[u8], pos: &mut usize) -> GDResult<Response> {
let packet_string = get_string_utf16_be(&buf, pos)?;
let split: Vec<&str> = packet_string.split("\x00").collect();
if split.len() != 5 {
return Err(GDError::PacketBad("Not right split size".to_string()));
}
let version_protocol = split[0].parse()
.map_err(|_| GDError::PacketBad("Expected int".to_string()))?;
let version_name = split[1].to_string();
let description = split[2].to_string();
let max_players = split[3].parse()
.map_err(|_| GDError::PacketBad("Expected int".to_string()))?;
let online_players = split[4].parse()
.map_err(|_| GDError::PacketBad("Expected int".to_string()))?;
Ok(Response {
version_name,
version_protocol,
max_players,
online_players,
sample_players: None,
description,
favicon: None,
previews_chat: None,
enforces_secure_chat: None,
server_type: Server::Legacy(LegacyGroup::V1_6)
})
}
fn get_info(&mut self) -> GDResult<Response> {
self.send_initial_request()?;
let buf = self.socket.receive(None)?;
let mut pos = 0;
if get_u8(&buf, &mut pos)? != 0xFF {
return Err(GDError::PacketBad("Expected 0xFF".to_string()));
}
let length = get_u16_be(&buf, &mut pos)? * 2;
if buf.len() != (length + 3) as usize { //+ 3 because of the first byte and the u16
return Err(GDError::PacketBad("Not right size".to_string()));
}
if !LegacyV1_6::is_protocol(&buf, &mut pos)? {
return Err(GDError::PacketBad("Not good".to_string()));
}
LegacyV1_6::get_response(&buf, &mut pos)
}
pub fn query(address: &str, port: u16, timeout_settings: Option<TimeoutSettings>) -> GDResult<Response> {
LegacyV1_6::new(address, port, timeout_settings)?.get_info()
}
}

View file

@ -0,0 +1,45 @@
use crate::{GDError, GDResult};
use crate::protocols::minecraft::{LegacyGroup, Response, Server};
use crate::protocols::minecraft::protocol::java::Java;
use crate::protocols::minecraft::protocol::legacy_v1_4::LegacyV1_4;
use crate::protocols::minecraft::protocol::legacy_v1_6::LegacyV1_6;
use crate::protocols::minecraft::protocol::legacy_bv1_8::LegacyBV1_8;
use crate::protocols::types::TimeoutSettings;
mod java;
mod legacy_v1_4;
mod legacy_v1_6;
mod legacy_bv1_8;
/// Queries a Minecraft server.
pub fn query(address: &str, port: u16, timeout_settings: Option<TimeoutSettings>) -> GDResult<Response> {
if let Ok(response) = query_specific(Server::Java, address, port, timeout_settings.clone()) {
return Ok(response);
}
if let Ok(response) = query_specific(Server::Legacy(LegacyGroup::V1_6), address, port, timeout_settings.clone()) {
return Ok(response);
}
if let Ok(response) = query_specific(Server::Legacy(LegacyGroup::V1_4), address, port, timeout_settings.clone()) {
return Ok(response);
}
if let Ok(response) = query_specific(Server::Legacy(LegacyGroup::VB1_8), address, port, timeout_settings.clone()) {
return Ok(response);
}
Err(GDError::AutoQuery("No protocol returned a response.".to_string()))
}
/// Queries a specific Minecraft Server type.
pub fn query_specific(mc_type: Server, address: &str, port: u16, timeout_settings: Option<TimeoutSettings>) -> GDResult<Response> {
match mc_type {
Server::Java => Java::query(address, port, timeout_settings),
Server::Legacy(category) => match category {
LegacyGroup::V1_6 => LegacyV1_6::query(address, port, timeout_settings),
LegacyGroup::V1_4 => LegacyV1_4::query(address, port, timeout_settings),
LegacyGroup::VB1_8 => LegacyBV1_8::query(address, port, timeout_settings),
}
}
}

View file

@ -0,0 +1,151 @@
/*
This file contains lightly modified versions of the original code. (using only the varint parts)
Code reference: https://github.com/thisjaiden/golden_apple/blob/master/src/lib.rs
MIT License
Copyright (c) 2021-2022 Jaiden Bernard
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.
*/
use crate::{GDError, GDResult};
use crate::utils::buffer::get_u8;
/// The type of Minecraft Server you want to query
#[derive(Debug)]
pub enum Server {
/// Java Edition
Java,
/// Legacy Java
Legacy(LegacyGroup)
}
/// Legacy Java (Versions) Groups
#[derive(Debug)]
pub enum LegacyGroup {
/// 1.6
V1_6,
/// 1.4 - 1.5
V1_4,
/// Beta 1.8 - 1.3
VB1_8
}
/// Information about a player
#[derive(Debug)]
pub struct Player {
pub name: String,
pub id: String
}
/// A query response
#[derive(Debug)]
pub struct Response {
/// Version name, example: "1.19.2"
pub version_name: String,
/// Version protocol, example: 760 (for 1.19.2)
pub version_protocol: i32,
/// Number of server capacity
pub max_players: u32,
/// Number of online players
pub online_players: u32,
/// Some online players (can be missing)
pub sample_players: Option<Vec<Player>>,
/// Server's description or MOTD
pub description: String,
/// The favicon (can be missing)
pub favicon: Option<String>,
/// Tells if the chat preview is enabled (can be missing)
pub previews_chat: Option<bool>,
/// Tells if secure chat is enforced (can be missing)
pub enforces_secure_chat: Option<bool>,
/// Tell's the server type
pub server_type: Server
}
pub fn get_varint(buf: &[u8], pos: &mut usize) -> GDResult<i32> {
let mut result = 0;
let msb: u8 = 0b10000000;
let mask: u8 = !msb;
for i in 0..5 {
let current_byte = get_u8(buf, pos)?;
result |= ((current_byte & mask) as i32) << (7 * i);
// The 5th byte is only allowed to have the 4 smallest bits set
if i == 4 && (current_byte & 0xf0 != 0) {
return Err(GDError::PacketBad("VarInt Overflow".to_string()))
}
if (current_byte & msb) == 0 {
break;
}
}
Ok(result)
}
pub fn as_varint(value: i32) -> Vec<u8> {
let mut bytes = vec![];
let mut reading_value = value;
let msb: u8 = 0b10000000;
let mask: i32 = 0b01111111;
for _ in 0..5 {
let tmp = (reading_value & mask) as u8;
reading_value &= !mask;
reading_value = reading_value.rotate_right(7);
if reading_value != 0 {
bytes.push(tmp | msb);
} else {
bytes.push(tmp);
break;
}
}
bytes
}
pub fn get_string(buf: &[u8], pos: &mut usize) -> GDResult<String> {
let length = get_varint(buf, pos)? as usize;
let mut text = vec![0; length];
for i in 0..length {
text[i] = get_u8(buf, pos)?;
}
Ok(String::from_utf8(text)
.map_err(|_| GDError::PacketBad("Minecraft bad String".to_string()))?)
}
pub fn as_string(value: String) -> Vec<u8> {
let mut buf = as_varint(value.len() as i32);
buf.extend(value.as_bytes().to_vec());
buf
}

View file

@ -4,7 +4,9 @@
//! A protocol will be here if it supports multiple entries, if not, its implementation will be
//! in that specific needed place, a protocol can be independently queried.
/// Reference: [Server Query](https://developer.valvesoftware.com/wiki/Server_queries)
pub mod valve;
/// General types that are used by all protocols.
pub mod types;
/// Reference: [Server Query](https://developer.valvesoftware.com/wiki/Server_queries)
pub mod valve;
/// Reference: [Server List Ping](https://wiki.vg/Server_List_Ping)
pub mod minecraft;

View file

@ -2,6 +2,7 @@ use std::time::Duration;
use crate::{GDError, GDResult};
/// Timeout settings for socket operations
#[derive(Clone)]
pub struct TimeoutSettings {
read: Option<Duration>,
write: Option<Duration>
@ -12,13 +13,13 @@ impl TimeoutSettings {
pub fn new(read: Option<Duration>, write: Option<Duration>) -> GDResult<Self> {
if let Some(read_duration) = read {
if read_duration == Duration::new(0, 0) {
return Err(GDError::InvalidInput("Can't pass duration 0 to timeout settings".to_owned()))
return Err(GDError::InvalidInput("Can't pass duration 0 to timeout settings".to_string()))
}
}
if let Some(write_duration) = write {
if write_duration == Duration::new(0, 0) {
return Err(GDError::InvalidInput("Can't pass duration 0 to timeout settings".to_owned()))
return Err(GDError::InvalidInput("Can't pass duration 0 to timeout settings".to_string()))
}
}

View file

@ -1,10 +1,10 @@
use std::net::UdpSocket;
use bzip2_rs::decoder::Decoder;
use crate::{GDError, GDResult};
use crate::protocols::types::TimeoutSettings;
use crate::protocols::valve::{App, ModData, SteamID};
use crate::protocols::valve::types::{Environment, ExtraData, GatheringSettings, Request, Response, Server, ServerInfo, ServerPlayer, ServerRule, TheShip};
use crate::utils::{buffer, complete_address, u8_lower_upper};
use crate::socket::{Socket, UdpSocket};
use crate::utils::{buffer, u8_lower_upper};
#[derive(Debug, Clone)]
struct Packet {
@ -42,7 +42,7 @@ impl Packet {
fn initial(kind: Request) -> Self {
Self {
header: 4294967295, //FF FF FF FF
kind: kind as u8,
kind: kind.clone() as u8,
payload: match kind {
Request::INFO => String::from("Source Engine Query\0").into_bytes(),
_ => vec![0xFF, 0xFF, 0xFF, 0xFF]
@ -140,49 +140,29 @@ impl SplitPacket {
}
struct ValveProtocol {
socket: UdpSocket,
complete_address: String
socket: UdpSocket
}
static PACKET_SIZE: usize = 1400;
impl ValveProtocol {
fn new(address: &str, port: u16, timeout_settings: TimeoutSettings) -> GDResult<Self> {
let socket = UdpSocket::bind("0.0.0.0:0").map_err(|e| GDError::SocketBind(e.to_string()))?;
socket.set_read_timeout(timeout_settings.get_read()).unwrap(); //unwrapping because TimeoutSettings::new
socket.set_write_timeout(timeout_settings.get_write()).unwrap();//checks if these are 0 and throws an error
fn new(address: &str, port: u16, timeout_settings: Option<TimeoutSettings>) -> GDResult<Self> {
let socket = UdpSocket::new(address, port)?;
socket.apply_timeout(timeout_settings)?;
Ok(Self {
socket,
complete_address: complete_address(address, port)?
socket
})
}
fn send(&self, data: &[u8]) -> GDResult<()> {
self.socket.send_to(&data, &self.complete_address).map_err(|e| GDError::PacketSend(e.to_string()))?;
Ok(())
}
fn receive_raw(&self, buffer_size: usize) -> GDResult<Vec<u8>> {
let mut buf: Vec<u8> = vec![0; buffer_size];
let (amt, _) = self.socket.recv_from(&mut buf.as_mut_slice()).map_err(|e| GDError::PacketReceive(e.to_string()))?;
if amt < 6 {
return Err(GDError::PacketUnderflow("Any Valve Protocol response can't be under 6 bytes long.".to_string()));
}
Ok(buf[..amt].to_vec())
}
fn receive(&self, app: &App, protocol: u8, buffer_size: usize) -> GDResult<Packet> {
let mut buf = self.receive_raw(buffer_size)?;
fn receive(&mut self, app: &App, protocol: u8, buffer_size: usize) -> GDResult<Packet> {
let mut buf = self.socket.receive(Some(buffer_size))?;
if buf[0] == 0xFE { //the packet is split
let mut main_packet = SplitPacket::new(&app, protocol, &buf)?;
for _ in 1..main_packet.total {
buf = self.receive_raw(buffer_size)?;
buf = self.socket.receive(Some(buffer_size))?;
let chunk_packet = SplitPacket::new(&app, protocol, &buf)?;
main_packet.payload.extend(chunk_packet.payload);
}
@ -195,10 +175,10 @@ impl ValveProtocol {
}
/// Ask for a specific request only.
fn get_request_data(&self, app: &App, protocol: u8, kind: Request) -> GDResult<Vec<u8>> {
fn get_request_data(&mut self, app: &App, protocol: u8, kind: Request) -> GDResult<Vec<u8>> {
let request_initial_packet = Packet::initial(kind.clone()).to_bytes();
self.send(&request_initial_packet)?;
self.socket.send(&request_initial_packet)?;
let packet = self.receive(app, protocol, PACKET_SIZE)?;
if packet.kind != 0x41 { //'A'
@ -208,7 +188,7 @@ impl ValveProtocol {
let challenge = packet.payload;
let challenge_packet = Packet::challenge(kind.clone(), challenge).to_bytes();
self.send(&challenge_packet)?;
self.socket.send(&challenge_packet)?;
Ok(self.receive(app, protocol, PACKET_SIZE)?.payload)
}
@ -216,11 +196,11 @@ impl ValveProtocol {
let mut pos = 0;
buffer::get_u8(&buf, &mut pos)?; //get the header (useless info)
buffer::get_string(&buf, &mut pos)?; //get the server address (useless info)
let name = buffer::get_string(&buf, &mut pos)?;
let map = buffer::get_string(&buf, &mut pos)?;
let folder = buffer::get_string(&buf, &mut pos)?;
let game = buffer::get_string(&buf, &mut pos)?;
buffer::get_string_utf8_le(&buf, &mut pos)?; //get the server address (useless info)
let name = buffer::get_string_utf8_le(&buf, &mut pos)?;
let map = buffer::get_string_utf8_le(&buf, &mut pos)?;
let folder = buffer::get_string_utf8_le(&buf, &mut pos)?;
let game = buffer::get_string_utf8_le(&buf, &mut pos)?;
let players = buffer::get_u8(&buf, &mut pos)?;
let max_players = buffer::get_u8(&buf, &mut pos)?;
let protocol = buffer::get_u8(&buf, &mut pos)?;
@ -240,8 +220,8 @@ impl ValveProtocol {
let mod_data = match is_mod {
false => None,
true => Some(ModData {
link: buffer::get_string(&buf, &mut pos)?,
download_link: buffer::get_string(&buf, &mut pos)?,
link: buffer::get_string_utf8_le(&buf, &mut pos)?,
download_link: buffer::get_string_utf8_le(&buf, &mut pos)?,
version: buffer::get_u32_le(&buf, &mut pos)?,
size: buffer::get_u32_le(&buf, &mut pos)?,
multiplayer_only: buffer::get_u8(&buf, &mut pos)? == 1,
@ -274,7 +254,7 @@ impl ValveProtocol {
}
/// Get the server information's.
fn get_server_info(&self, app: &App) -> GDResult<ServerInfo> {
fn get_server_info(&mut self, app: &App) -> GDResult<ServerInfo> {
let buf = self.get_request_data(&app, 0, Request::INFO)?;
if let App::GoldSrc(force) = app {
if *force {
@ -285,10 +265,10 @@ impl ValveProtocol {
let mut pos = 0;
let protocol = buffer::get_u8(&buf, &mut pos)?;
let name = buffer::get_string(&buf, &mut pos)?;
let map = buffer::get_string(&buf, &mut pos)?;
let folder = buffer::get_string(&buf, &mut pos)?;
let game = buffer::get_string(&buf, &mut pos)?;
let name = buffer::get_string_utf8_le(&buf, &mut pos)?;
let map = buffer::get_string_utf8_le(&buf, &mut pos)?;
let folder = buffer::get_string_utf8_le(&buf, &mut pos)?;
let game = buffer::get_string_utf8_le(&buf, &mut pos)?;
let mut appid = buffer::get_u16_le(&buf, &mut pos)? as u32;
let players = buffer::get_u8(&buf, &mut pos)?;
let max_players = buffer::get_u8(&buf, &mut pos)?;
@ -315,7 +295,7 @@ impl ValveProtocol {
duration: buffer::get_u8(&buf, &mut pos)?
})
};
let version = buffer::get_string(&buf, &mut pos)?;
let version = buffer::get_string_utf8_le(&buf, &mut pos)?;
let extra_data = match buffer::get_u8(&buf, &mut pos) {
Err(_) => None,
Ok(value) => Some(ExtraData {
@ -333,11 +313,11 @@ impl ValveProtocol {
},
tv_name: match (value & 0x40) > 0 {
false => None,
true => Some(buffer::get_string(&buf, &mut pos)?)
true => Some(buffer::get_string_utf8_le(&buf, &mut pos)?)
},
keywords: match (value & 0x20) > 0 {
false => None,
true => Some(buffer::get_string(&buf, &mut pos)?)
true => Some(buffer::get_string_utf8_le(&buf, &mut pos)?)
},
game_id: match (value & 0x01) > 0 {
false => None,
@ -374,17 +354,17 @@ impl ValveProtocol {
}
/// Get the server player's.
fn get_server_players(&self, app: &App, protocol: u8) -> GDResult<Vec<ServerPlayer>> {
fn get_server_players(&mut self, app: &App, protocol: u8) -> GDResult<Vec<ServerPlayer>> {
let buf = self.get_request_data(&app, protocol, Request::PLAYERS)?;
let mut pos = 0;
let count = buffer::get_u8(&buf, &mut pos)?;
let mut players: Vec<ServerPlayer> = Vec::new();
let count = buffer::get_u8(&buf, &mut pos)? as usize;
let mut players: Vec<ServerPlayer> = Vec::with_capacity(count);
for _ in 0..count {
pos += 1; //skip the index byte
players.push(ServerPlayer {
name: buffer::get_string(&buf, &mut pos)?,
name: buffer::get_string_utf8_le(&buf, &mut pos)?,
score: buffer::get_u32_le(&buf, &mut pos)?,
duration: buffer::get_f32_le(&buf, &mut pos)?,
deaths: match *app == SteamID::TS.as_app() {
@ -402,7 +382,7 @@ impl ValveProtocol {
}
/// Get the server rules's.
fn get_server_rules(&self, app: &App, protocol: u8) -> GDResult<Option<Vec<ServerRule>>> {
fn get_server_rules(&mut self, app: &App, protocol: u8) -> GDResult<Option<Vec<ServerRule>>> {
if *app == SteamID::CSGO.as_app() { //cause csgo wont respond to this since feb 21 2014 update
return Ok(None);
}
@ -410,13 +390,13 @@ impl ValveProtocol {
let buf = self.get_request_data(&app, protocol, Request::RULES)?;
let mut pos = 0;
let count = buffer::get_u16_le(&buf, &mut pos)?;
let mut rules: Vec<ServerRule> = Vec::new();
let count = buffer::get_u16_le(&buf, &mut pos)? as usize;
let mut rules: Vec<ServerRule> = Vec::with_capacity(count);
for _ in 0..count {
rules.push(ServerRule {
name: buffer::get_string(&buf, &mut pos)?,
value: buffer::get_string(&buf, &mut pos)?
name: buffer::get_string_utf8_le(&buf, &mut pos)?,
value: buffer::get_string_utf8_le(&buf, &mut pos)?
})
}
@ -428,12 +408,11 @@ impl ValveProtocol {
/// Providing None to the settings results in using the default values for them (GatherSettings::[default](GatheringSettings::default), TimeoutSettings::[default](TimeoutSettings::default)).
pub fn query(address: &str, port: u16, app: App, gather_settings: Option<GatheringSettings>, timeout_settings: Option<TimeoutSettings>) -> GDResult<Response> {
let response_gather_settings = gather_settings.unwrap_or(GatheringSettings::default());
let response_timeout_settings = timeout_settings.unwrap_or(TimeoutSettings::default());
get_response(address, port, app, response_gather_settings, response_timeout_settings)
get_response(address, port, app, response_gather_settings, timeout_settings)
}
fn get_response(address: &str, port: u16, app: App, gather_settings: GatheringSettings, timeout_settings: TimeoutSettings) -> GDResult<Response> {
let client = ValveProtocol::new(address, port, timeout_settings)?;
fn get_response(address: &str, port: u16, app: App, gather_settings: GatheringSettings, timeout_settings: Option<TimeoutSettings>) -> GDResult<Response> {
let mut client = ValveProtocol::new(address, port, timeout_settings)?;
let info = client.get_server_info(&app)?;
let protocol = info.protocol;

View file

@ -1,5 +1,3 @@
use std::time::Duration;
use crate::{GDError, GDResult};
/// The type of the server.
#[derive(Debug)]

88
src/socket.rs Normal file
View file

@ -0,0 +1,88 @@
use std::io::{Read, Write};
use std::net;
use crate::{GDError, GDResult};
use crate::protocols::types::TimeoutSettings;
use crate::utils::complete_address;
static DEFAULT_PACKET_SIZE: usize = 1024;
pub trait Socket {
fn new(address: &str, port: u16) -> GDResult<Self> where Self: Sized;
fn apply_timeout(&self, timeout_settings: Option<TimeoutSettings>) -> GDResult<()>;
fn send(&mut self, data: &[u8]) -> GDResult<()>;
fn receive(&mut self, size: Option<usize>) -> GDResult<Vec<u8>>;
}
pub struct TcpSocket {
socket: net::TcpStream
}
impl Socket for TcpSocket {
fn new(address: &str, port: u16) -> GDResult<Self> {
let complete_address = complete_address(address, port)?;
let socket = net::TcpStream::connect(complete_address).map_err(|e| GDError::SocketConnect(e.to_string()))?;
Ok(Self {
socket
})
}
fn apply_timeout(&self, timeout_settings: Option<TimeoutSettings>) -> GDResult<()> {
let settings = timeout_settings.unwrap_or(TimeoutSettings::default());
self.socket.set_read_timeout(settings.get_read()).unwrap(); //unwrapping because TimeoutSettings::new
self.socket.set_write_timeout(settings.get_write()).unwrap(); //checks if these are 0 and throws an error
Ok(())
}
fn send(&mut self, data: &[u8]) -> GDResult<()> {
self.socket.write(&data).map_err(|e| GDError::PacketSend(e.to_string()))?;
Ok(())
}
fn receive(&mut self, size: Option<usize>) -> GDResult<Vec<u8>> {
let mut buf = Vec::with_capacity(size.unwrap_or(DEFAULT_PACKET_SIZE));
self.socket.read_to_end(&mut buf).map_err(|e| GDError::PacketReceive(e.to_string()))?;
Ok(buf)
}
}
pub struct UdpSocket {
socket: net::UdpSocket,
complete_address: String
}
impl Socket for UdpSocket {
fn new(address: &str, port: u16) -> GDResult<Self> {
let complete_address = complete_address(address, port)?;
let socket = net::UdpSocket::bind("0.0.0.0:0").map_err(|e| GDError::SocketBind(e.to_string()))?;
Ok(Self {
socket,
complete_address
})
}
fn apply_timeout(&self, timeout_settings: Option<TimeoutSettings>) -> GDResult<()> {
let settings = timeout_settings.unwrap_or(TimeoutSettings::default());
self.socket.set_read_timeout(settings.get_read()).unwrap(); //unwrapping because TimeoutSettings::new
self.socket.set_write_timeout(settings.get_write()).unwrap(); //checks if these are 0 and throws an error
Ok(())
}
fn send(&mut self, data: &[u8]) -> GDResult<()> {
self.socket.send_to(&data, &self.complete_address).map_err(|e| GDError::PacketSend(e.to_string()))?;
Ok(())
}
fn receive(&mut self, size: Option<usize>) -> GDResult<Vec<u8>> {
let mut buf: Vec<u8> = vec![0; size.unwrap_or(DEFAULT_PACKET_SIZE)];
let (number_of_bytes_received, _) = self.socket.recv_from(&mut buf).map_err(|e| GDError::PacketReceive(e.to_string()))?;
Ok(buf[..number_of_bytes_received].to_vec())
}
}

View file

@ -43,6 +43,16 @@ pub mod buffer {
Ok(value)
}
pub fn get_u16_be(buf: &[u8], pos: &mut usize) -> GDResult<u16> {
if buf.len() <= *pos + 1 {
return Err(GDError::PacketUnderflow("Unexpectedly short packet.".to_string()));
}
let value = u16::from_be_bytes([buf[*pos], buf[*pos + 1]]);
*pos += 2;
Ok(value)
}
pub fn get_u32_le(buf: &[u8], pos: &mut usize) -> GDResult<u32> {
if buf.len() <= *pos + 3 {
return Err(GDError::PacketUnderflow("Unexpectedly short packet.".to_string()));
@ -73,10 +83,25 @@ pub mod buffer {
Ok(value)
}
pub fn get_string(buf: &[u8], pos: &mut usize) -> GDResult<String> {
pub fn get_string_utf8_le(buf: &[u8], pos: &mut usize) -> GDResult<String> {
let sub_buf = &buf[*pos..];
let first_null_position = sub_buf.iter().position(|&x| x == 0).ok_or(GDError::PacketBad("Unexpectedly formatted packet.".to_string()))?;
let value = std::str::from_utf8(&sub_buf[..first_null_position]).unwrap().to_string();
let first_null_position = sub_buf.iter().position(|&x| x == 0)
.ok_or(GDError::PacketBad("Unexpectedly formatted packet.".to_string()))?;
let value = std::str::from_utf8(&sub_buf[..first_null_position])
.map_err(|_| GDError::PacketBad("Badly formatted string.".to_string()))?.to_string();
*pos += value.len() + 1;
Ok(value)
}
pub fn get_string_utf16_be(buf: &[u8], pos: &mut usize) -> GDResult<String> {
let sub_buf = &buf[*pos..];
let paired_buf: Vec<u16> = sub_buf.chunks_exact(2)
.into_iter().map(|a| u16::from_be_bytes([a[0], a[1]])).collect();
let value = String::from_utf16(&paired_buf)
.map_err(|_| GDError::PacketBad("Badly formatted string.".to_string()))?.to_string();
*pos += value.len() + 1;
Ok(value)
}
@ -143,12 +168,12 @@ mod tests {
}
#[test]
fn get_string_test() {
fn get_string_utf8_le_test() {
let data = [72, 101, 108, 108, 111, 0, 72];
let mut pos = 0;
assert_eq!(buffer::get_string(&data, &mut pos).unwrap(), "Hello");
assert_eq!(buffer::get_string_utf8_le(&data, &mut pos).unwrap(), "Hello");
assert_eq!(pos, 6);
assert!(buffer::get_string(&data, &mut pos).is_err());
assert!(buffer::get_string_utf8_le(&data, &mut pos).is_err());
assert_eq!(pos, 6);
}
}