From e869f8cf473a9b583fc31c9fe55c35ce157d2769 Mon Sep 17 00:00:00 2001 From: ElGusanitoPLAY Date: Sun, 27 Dec 2020 22:05:30 +0100 Subject: [PATCH 1/2] First upload of the big chungus --- Cargo.toml | 1 + examples/btf3login.rs | 19 +++ examples/factorio.rs | 16 ++- examples/minecraft.rs | 33 ++--- src/battlefield3/connection.rs | 101 ++++++++++++++++ src/battlefield3/mod.rs | 4 + src/battlefield3/packet.rs | 162 +++++++++++++++++++++++++ src/connection.rs | 38 ++++++ src/error.rs | 20 ++++ src/factorio/connection.rs | 106 +++++++++++++++++ src/factorio/mod.rs | 4 + src/factorio/packet.rs | 120 +++++++++++++++++++ src/lib.rs | 212 +++------------------------------ src/minecraft/connection.rs | 137 +++++++++++++++++++++ src/minecraft/mod.rs | 4 + src/minecraft/packet.rs | 120 +++++++++++++++++++ src/packet.rs | 131 +++++--------------- 17 files changed, 895 insertions(+), 333 deletions(-) create mode 100644 examples/btf3login.rs create mode 100644 src/battlefield3/connection.rs create mode 100644 src/battlefield3/mod.rs create mode 100644 src/battlefield3/packet.rs create mode 100644 src/connection.rs create mode 100644 src/error.rs create mode 100644 src/factorio/connection.rs create mode 100644 src/factorio/mod.rs create mode 100644 src/factorio/packet.rs create mode 100644 src/minecraft/connection.rs create mode 100644 src/minecraft/mod.rs create mode 100644 src/minecraft/packet.rs diff --git a/Cargo.toml b/Cargo.toml index ce9e895..3c54a42 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,6 +9,7 @@ repository = "https://github.com/panicbit/rust-rcon" edition = "2018" [dependencies] +async-trait = "0.1.42" err-derive = "0.3.0" tokio = { version = "1.0.0", features = ["net", "io-util", "time", "macros"] } diff --git a/examples/btf3login.rs b/examples/btf3login.rs new file mode 100644 index 0000000..f2b53c0 --- /dev/null +++ b/examples/btf3login.rs @@ -0,0 +1,19 @@ +use rcon::battlefield3::Btf3Connection; +use rcon::prelude::*; + +#[tokio::main] +async fn main() -> Result<()> { + let address = "127.0.0.1:47201"; + let mut conn = Btf3Connection::connect(address, "root").await?; + + demo(&mut conn, "serverInfo").await?; + + Ok(()) +} + +async fn demo(conn: &mut Btf3Connection, cmd: &str) -> Result<()> { + println!("request: {}", cmd); + let resp = conn.cmd(cmd).await?; + println!("response: {}", resp); + Ok(()) +} \ No newline at end of file diff --git a/examples/factorio.rs b/examples/factorio.rs index 84bcf66..3601a39 100644 --- a/examples/factorio.rs +++ b/examples/factorio.rs @@ -1,22 +1,20 @@ -use rcon::{Connection, Error}; +use rcon::factorio::FactorioConnection; +use rcon::prelude::*; #[tokio::main] -async fn main() -> Result<(), Error> { - let address = "localhost:1234"; - let mut conn = Connection::builder() - .enable_factorio_quirks(true) - .connect(address, "test").await?; +async fn main() -> Result<()> { + let address = "127.0.0.1:1234"; + let mut conn = FactorioConnection::connect(address, "test").await?; demo(&mut conn, "/c print('hello')").await?; demo(&mut conn, "/c print('world')").await?; - println!("commands finished"); Ok(()) } -async fn demo(conn: &mut Connection, cmd: &str) -> Result<(), Error> { +async fn demo(conn: &mut FactorioConnection, cmd: &str) -> Result<()> { println!("request: {}", cmd); let resp = conn.cmd(cmd).await?; println!("response: {}", resp); Ok(()) -} +} \ No newline at end of file diff --git a/examples/minecraft.rs b/examples/minecraft.rs index cc29b62..c29ca5b 100644 --- a/examples/minecraft.rs +++ b/examples/minecraft.rs @@ -1,35 +1,20 @@ -// Copyright (c) 2015 [rust-rcon developers] -// Licensed under the Apache License, Version 2.0 -// or the MIT -// license , -// at your option. All files in the project carrying such -// notice may not be copied, modified, or distributed except -// according to those terms. - -use rcon::{Connection, Error}; - -/* - This example expects a Minecraft with rcon enabled on port 25575 - and the rcon password "test" -*/ +use rcon::minecraft::MinecraftConnection; +use rcon::prelude::*; #[tokio::main] -async fn main() -> Result<(), Error> { - let address = "localhost:25575"; - let mut conn = Connection::builder() - .enable_minecraft_quirks(true) - .connect(address, "test").await?; +async fn main() -> Result<()> { + let address = "127.0.0.1:1234"; + let mut conn = MinecraftConnection::connect(address, "test").await?; demo(&mut conn, "list").await?; demo(&mut conn, "say Rust lang rocks! ;P").await?; demo(&mut conn, "save-all").await?; - //demo(&mut conn, "stop"); + Ok(()) } -async fn demo(conn: &mut Connection, cmd: &str) -> Result<(), Error> { +async fn demo(conn: &mut MinecraftConnection, cmd: &str) -> Result<()> { let resp = conn.cmd(cmd).await?; - println!("{}", resp); + println!("Response: {}", resp); Ok(()) -} +} \ No newline at end of file diff --git a/src/battlefield3/connection.rs b/src/battlefield3/connection.rs new file mode 100644 index 0000000..ab8841e --- /dev/null +++ b/src/battlefield3/connection.rs @@ -0,0 +1,101 @@ +use tokio::net::{TcpStream, ToSocketAddrs}; +use std::io; +use async_trait::async_trait; + +use crate::connection::Connection; +use crate::error::{Result, Error}; +use crate::packet::{Packet, PacketType}; +use super::packet::Btf3Packet; + +/// If the size in bytes of the packet is above this the server will reject +/// the connection +const BTF3_MAX_PAYLOAD_SIZE: usize = 16384; + +/// I dunno if it should be 1 or 0 but `it works` +const INITIAL_PACKET_ID: usize = 1; + +/// Representation of the client connection to a Remote Administration Interface +pub struct Btf3Connection { + stream: TcpStream, + next_packet_id: i32, +} + +impl Btf3Connection { + /// Auth into the server given the password, if the password is `""` it will + /// skip the login + async fn auth(&mut self, password: &str) -> Result { + if password == "" { + println!("Skipped login"); + Ok(true) + } else if self.cmd( + &format!("login.PlainText {}", password) + ).await?.contains("OK") + { + println!("Logged in"); + Ok(true) + } else { + Err(Error::Auth) + } + } +} + +#[async_trait] +impl Connection for Btf3Connection { + type Packet = Btf3Packet; + /// Connects to a rcon server, if the password if `""` if will skip + /// the login + async fn connect(address: T, password: &str) -> Result { + let stream = TcpStream::connect(address).await?; + let mut conn = Btf3Connection { + stream, + next_packet_id: INITIAL_PACKET_ID as i32, + }; + + conn.auth(password).await?; + Ok(conn) + } + + /// Send a certain command, in the implementation the `command` should be + /// parsed to a packet ready bytes buffer and sended via `send_packet` + async fn cmd(&mut self, command: &str) -> Result { + if command.len() > BTF3_MAX_PAYLOAD_SIZE { + return Err(Error::CommandTooLong); + } + self.next_packet_id = self.send_packet( + Btf3Packet::new( + self.next_packet_id, + PacketType::Request(0), + command.to_owned() + ) + ).await?; + + let response = self.receive_response().await?; + + Ok(response) + } + + /// Receives a response from the rcon server + async fn receive_response(&mut self) -> io::Result { + let received_packet = self.receive_packet().await?; + Ok(received_packet.get_body().into()) + } + + /// Low level function that send a Packet, returns the `id` of the sended + /// packet to be incremented + async fn send_packet(&mut self, packet: Self::Packet) -> io::Result { + let id = match self.next_packet_id + 1 { + n if n & 0x3fff != 0 => INITIAL_PACKET_ID as i32, + n => n, + }; + + packet.serialize(&mut self.stream).await?; + + Ok(id) + } + + /// Low level function that receives a Packet + async fn receive_packet(&mut self) -> io::Result { + Btf3Packet::deserialize(&mut self.stream).await + } +} + diff --git a/src/battlefield3/mod.rs b/src/battlefield3/mod.rs new file mode 100644 index 0000000..a123d06 --- /dev/null +++ b/src/battlefield3/mod.rs @@ -0,0 +1,4 @@ +mod connection; +mod packet; + +pub use connection::Btf3Connection; \ No newline at end of file diff --git a/src/battlefield3/packet.rs b/src/battlefield3/packet.rs new file mode 100644 index 0000000..3c98d54 --- /dev/null +++ b/src/battlefield3/packet.rs @@ -0,0 +1,162 @@ +use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}; +use async_trait::async_trait; +use std::io; + +use crate::packet::{Packet, PacketType}; + +/// ### The Battlefield packets are designed in this way: +/// i32 (le) i32 (le) i32 (le) [u8; packet_size - sizeof(i32)*3] +/// ------------------------------------------------------------------------- +/// | sequence | packet_size | word_count | body | +/// ------------------------------------------------------------------------ +/// +/// Knowing that there are also two special fields: +/// +/// #### SEQUENCE: +/// 0 29 30 31 (i32 bits) +/// ---------------------------------------------------- +/// | id | type | origin | +/// ---------------------------------------------------- +/// id: number that grows apart from the client and the server, the spec +/// doesn't say initial number so we will be using 1 in this +/// implementation +/// origin: if this bit = 0, its originated from the server, if its = 1 +/// its originated from the client (us) +/// type: 0 = Request, 1 = Response, ussually we are the request and the +/// server just response +/// +/// #### BODY: +/// The body is composed by a determined number of words and each word +/// have the following design +/// i32 (le) [u8; word_size] u8 +/// ------------------------------------------------------------ +/// | word_size | word | null | +/// ----------------------------------------------------------- +/// NOTE: note that word can only contain ASCII characters and the null +/// terminator is not counted in the word_size +#[derive(Debug)] +pub struct Btf3Packet { + /// This 3 fields are copied without needed modifications + sequence: i32, + packet_size: i32, + word_count: i32, + + /// This String must be converted to the required specification at the send + /// time, here the words are separated by spaces + body: String, + + /// The packet type + ptype: PacketType, +} + +impl Btf3Packet { + /// Instantiates a new Btf3Packet + pub fn new(id: i32, ptype: PacketType, body: String) -> Self { + // NOTE: This still does not discern the origin of the packet it + // suposses that the reponses always come from the server and the + // requests from the client + let sequence = match ptype { + PacketType::Request(_) => id, + PacketType::Response(_) => 3 << 29 | id, + _ => todo!(), + }; + + let body_size: usize = body + .split_ascii_whitespace() + .map(|s| s.len()) + .fold(0, |acc, x| acc + x + 5); + + let packet_size = 4 * 3 + body_size as i32; + Btf3Packet { + sequence, + packet_size, + word_count: body.split_ascii_whitespace().count() as i32, + body, + ptype, + } + } +} + +#[async_trait] +impl Packet for Btf3Packet { + /// Checks if the packet is an error, probably just useful for Responses + fn is_error(&self) -> bool { + !self.get_body().contains("OK") + } + + /// Serializes de packets, aka convert and send + async fn serialize(&self, w: &mut T) -> io::Result<()> { + let mut buf = Vec::with_capacity(self.packet_size as usize); + buf.write_i32_le(self.sequence).await?; + buf.write_i32_le(self.packet_size).await?; + buf.write_i32_le(self.word_count).await?; + for word_str in self.body.split_ascii_whitespace() { + buf.write_i32_le(word_str.len() as i32).await?; + buf.write_all(word_str.as_bytes()).await?; + buf.write_all(b"\x00").await?; + } + + w.write_all(&buf).await?; + + Ok(()) + } + + /// Deserializes de packets, aka receive and convert + async fn deserialize(r: &mut T) -> io::Result { + let sequence = r.read_i32_le().await?; + let (_id, ptype) = (sequence & 0xC000, match sequence >> 29 { + 0b10 | 0b11 => PacketType::Response(1), + 0b00 | 0b01 => PacketType::Request(0), + // TODO: more sofisticated error report + _ => return Err(io::Error::from(io::ErrorKind::Other)) + }); + + let packet_size = r.read_i32_le().await?; + let word_count = r.read_i32_le().await?; + + // Overallocation by 4*3 + let mut body = Vec::with_capacity(packet_size as usize); + + for _ in 0..word_count { + let word_size = r.read_i32_le().await?; + let mut word_buffer = Vec::with_capacity(word_size as usize); + r.take(word_size as u64) + .read_to_end(&mut word_buffer) + .await?; + body.extend_from_slice(&word_buffer); + body.push(' ' as u8); + r.read_u8().await?; + } + body.pop(); + let body = String::from_utf8(body) + .map_err(|_| io::Error::from(io::ErrorKind::InvalidData))?; + + Ok( + Btf3Packet { + sequence, + packet_size, + word_count, + body, + ptype, + } + ) + } + + /// Gets the body of the packet + #[inline] + fn get_body(&self) -> &str { + &self.body + } + /// Gets the packet type + #[inline] + fn get_type(&self) -> PacketType { + self.ptype + } + /// Returns the id of the packet and also increments it + #[inline] + fn get_id(&self) -> i32 { + self.sequence & 0xC000 + } +} + +unsafe impl Send for Btf3Packet {} \ No newline at end of file diff --git a/src/connection.rs b/src/connection.rs new file mode 100644 index 0000000..5efe253 --- /dev/null +++ b/src/connection.rs @@ -0,0 +1,38 @@ +use super::error::Result; +use std::io; +use tokio::net::ToSocketAddrs; +use async_trait::async_trait; + +/// NOTE: the `#[async_trait]` macro is to be able to declare async functions +/// NOTE: The connection at least must contain two fields +/// - stream: TcpStream +/// - id: i32 +/// Representation of an Rcon Connection, in the high level it con `connect` +/// with or without password and the it can send commnands `cmd` and it can +/// `receive_response`, also some low level functions like `receive_packet` and +/// `send_packet` are needed to them be wrapped inside the high level ones +#[async_trait] +pub trait Connection: Sized { + type Packet; + /// Connects to a rcon server, if the password is mandatory is implementation + /// specific, but ensure in the implementation to allow empty string `""` + /// for no password provided and no auth needed + async fn connect(address: T, password: &str) -> Result; + + /// Send a certain command, in the implementation the `command` should be + /// parsed to a packet ready bytes buffer and sended via `send_packet` + async fn cmd(&mut self, command: &str) -> Result; + + /// Receives a response from the rcon server, in the implementation it must + /// call receive packet as many times it requires parse the body and present + /// it as a clear response String + async fn receive_response(&mut self) -> io::Result; + + /// Low level function that send a Packet, returns the `id` of the sended + /// packet to be incremented + async fn send_packet(&mut self, packet: Self::Packet) -> io::Result; + + /// Low level function that receives a Packet + async fn receive_packet(&mut self) -> io::Result; +} + diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..85a26d6 --- /dev/null +++ b/src/error.rs @@ -0,0 +1,20 @@ +use err_derive::Error; +use std::{ + io, + result, +}; + +/// The different kind of errors that can happend while interacting with the +/// rcon server +#[derive(Debug, Error)] +pub enum Error { + #[error(display = "authentication failed")] + Auth, + #[error(display = "command exceeds the maximum length")] + CommandTooLong, + #[error(display = "{}", _0)] + Io(#[error(source)] io::Error), +} + +/// The error result used throught the library +pub type Result = result::Result; \ No newline at end of file diff --git a/src/factorio/connection.rs b/src/factorio/connection.rs new file mode 100644 index 0000000..7df9ece --- /dev/null +++ b/src/factorio/connection.rs @@ -0,0 +1,106 @@ +use tokio::net::{TcpStream, ToSocketAddrs}; +use std::io; +use async_trait::async_trait; + +use crate::connection::Connection; +use crate::error::{Result, Error}; +use crate::packet::{Packet, PacketType}; +use super::packet::FactorioPacket; + +/// If the size in bytes of the packet is above this the server will reject +/// the connection +/// TODO: +// const FACTORIO_MAX_PAYLOAD_SIZE: usize = ...; + +/// I dunno if it should be 1 or 0 but `it works` +const INITIAL_PACKET_ID: usize = 1; + +/// Representation of the client connection to a Remote Administration Interface +pub struct FactorioConnection { + stream: TcpStream, + next_packet_id: i32, +} + +impl FactorioConnection { + /// Auth into the server given the password, login is mandatory + async fn auth(&mut self, password: &str) -> Result<()> { + self.send_packet( + FactorioPacket::new( + self.next_packet_id, + PacketType::Request(3), + password.to_string(), + ) + ).await?; + + let received_packet = loop { + let received_packet = self.receive_packet().await?; + if let PacketType::Response(2) = received_packet.get_type() { + break received_packet; + } + }; + + if received_packet.is_error() { + Err(Error::Auth) + } else { + Ok(()) + } + } +} + +#[async_trait] +impl Connection for FactorioConnection { + type Packet = FactorioPacket; + /// Connects to a rcon server, login is mandatory + async fn connect(address: T, password: &str) -> Result { + let stream = TcpStream::connect(address).await?; + let mut conn = FactorioConnection { + stream, + next_packet_id: INITIAL_PACKET_ID as i32, + }; + + conn.auth(password).await?; + Ok(conn) + } + + /// Send a certain command, in the implementation the `command` should be + /// parsed to a packet ready bytes buffer and sended via `send_packet` + async fn cmd(&mut self, command: &str) -> Result { + self.next_packet_id = self.send_packet( + FactorioPacket::new( + self.next_packet_id, + PacketType::Request(0), + command.to_owned() + ) + ).await?; + + let response = self.receive_response().await?; + + Ok(response) + } + + /// Receives a response from the rcon server + async fn receive_response(&mut self) -> io::Result { + let received_packet = self.receive_packet().await?; + Ok(received_packet.get_body().into()) + } + + /// Low level function that send a Packet, returns the `id` of the sended + /// packet to be incremented + async fn send_packet(&mut self, packet: Self::Packet) -> io::Result { + // I dont know if factorio uses some bits for something in particular + let id = match self.next_packet_id + 1 { + n if n & 0x3fff != 0 => INITIAL_PACKET_ID as i32, + n => n, + }; + + packet.serialize(&mut self.stream).await?; + + Ok(id) + } + + /// Low level function that receives a Packet + async fn receive_packet(&mut self) -> io::Result { + FactorioPacket::deserialize(&mut self.stream).await + } +} + diff --git a/src/factorio/mod.rs b/src/factorio/mod.rs new file mode 100644 index 0000000..1485a1a --- /dev/null +++ b/src/factorio/mod.rs @@ -0,0 +1,4 @@ +mod packet; +mod connection; + +pub use connection::FactorioConnection; \ No newline at end of file diff --git a/src/factorio/packet.rs b/src/factorio/packet.rs new file mode 100644 index 0000000..0bedf8c --- /dev/null +++ b/src/factorio/packet.rs @@ -0,0 +1,120 @@ +use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}; +use async_trait::async_trait; +use std::io; + +use crate::packet::{Packet, PacketType}; + +/// #### Factorio packets are designed in this way: +/// +/// TODO: ... +/// +#[derive(Debug)] +pub struct FactorioPacket { + id: i32, + packet_size: i32, + + /// This String must be converted to the required specification at the send + /// time, here the words are separated by spaces + body: String, + + /// The packet type + ptype_n: i32, +} + +impl FactorioPacket { + /// Instantiates a new FactorioPacket + pub fn new(id: i32, ptype: PacketType, body: String) -> Self { + // NOTE: This still does not discern the origin of the packet it + // suposses that the reponses always come from the server and the + // requests from the client + + let packet_size = 10 + body.len() as i32; + let ptype_n = match ptype { + PacketType::Request(n) => n, + PacketType::Response(n) => n, + PacketType::Custom(_) => todo!(), + }; + + FactorioPacket { + id, + packet_size, + body, + ptype_n, + } + } +} + +#[async_trait] +impl Packet for FactorioPacket { + /// Checks if the packet is an error, probably just useful for Responses + fn is_error(&self) -> bool { + self.id < 0 + } + + /// Serializes de packets, aka convert and send + async fn serialize(&self, w: &mut T) -> io::Result<()> { + let mut buf = Vec::with_capacity(self.packet_size as usize); + + buf.write_i32_le(self.packet_size).await?; + buf.write_i32_le(self.id).await?; + buf.write_i32_le(self.ptype_n).await?; + buf.write_all(self.body.as_bytes()).await?; + buf.write_all(b"\x00\x00").await?; + + w.write_all(&buf).await?; + + Ok(()) + } + + /// Deserializes de packets, aka receive and convert + async fn deserialize(r: &mut T) -> io::Result { + let packet_size = r.read_i32_le().await?; + let id = r.read_i32_le().await?; + let ptype_n = r.read_i32_le().await?; + let body_length = packet_size - 10; + + let mut body = Vec::with_capacity(body_length as usize); + + r.take(body_length as u64) + .read_to_end(&mut body) + .await?; + + let body = String::from_utf8(body) + .map_err(|_| io::Error::from(io::ErrorKind::InvalidData))?; + + // Terminated nulls (\x00\x00) + r.read_u16().await?; + + Ok( + FactorioPacket { + id, + packet_size, + body, + ptype_n, + } + ) + } + + /// Gets the body of the packet + #[inline] + fn get_body(&self) -> &str { + &self.body + } + + /// Gets the packet type + #[inline] + fn get_type(&self) -> PacketType { + match self.ptype_n { + 0 => PacketType::Response(0), + 2 => PacketType::Request(2), + 3 => PacketType::Request(3), + _ => todo!(), + } + } + + /// Returns the id of the packet and also increments it + #[inline] + fn get_id(&self) -> i32 { + self.id + } +} \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index e01a25e..d9ad075 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,200 +1,16 @@ -// Copyright (c) 2015 [rust-rcon developers] -// Licensed under the Apache License, Version 2.0 -// or the MIT -// license , -// at your option. All files in the project carrying such -// notice may not be copied, modified, or distributed except -// according to those terms. - -use err_derive::Error; -use packet::{Packet, PacketType}; -use std::io; -use std::time::Duration; -use tokio::net::{TcpStream, ToSocketAddrs}; - mod packet; - -const INITIAL_PACKET_ID: i32 = 1; -const DELAY_TIME_MILLIS: u64 = 3; -const MINECRAFT_MAX_PAYLOAD_SIZE: usize = 1413; - -#[derive(Debug, Error)] -pub enum Error { - #[error(display = "authentication failed")] - Auth, - #[error(display = "command exceeds the maximum length")] - CommandTooLong, - #[error(display = "{}", _0)] - Io(#[error(source)] io::Error), -} - -pub type Result = std::result::Result; - -pub struct Connection { - stream: TcpStream, - next_packet_id: i32, - minecraft_quirks_enabled: bool, - factorio_quirks_enabled: bool, -} - -impl Connection { - /// Create a connectiion builder. - /// Allows configuring the rcon connection. - pub fn builder() -> Builder { - Builder::new() - } - - /// Connect to an rcon server. - /// By default this enables minecraft quirks. - /// If you need to customize this behaviour, use a Builder. - pub async fn connect(address: T, password: &str) -> Result { - Self::builder() - .enable_minecraft_quirks(true) - .connect(address, password).await - } - - pub async fn cmd(&mut self, cmd: &str) -> Result { - if self.minecraft_quirks_enabled && cmd.len() > MINECRAFT_MAX_PAYLOAD_SIZE { - return Err(Error::CommandTooLong); - } - - self.send(PacketType::ExecCommand, cmd).await?; - - if self.minecraft_quirks_enabled { - tokio::time::sleep(Duration::from_millis(DELAY_TIME_MILLIS)).await; - } - - let response = self.receive_response().await?; - - Ok(response) - } - - async fn receive_response(&mut self) -> Result { - if self.factorio_quirks_enabled { - self.receive_single_packet_response().await - } else { - self.receive_multi_packet_response().await - } - } - - async fn receive_single_packet_response(&mut self) -> Result { - let received_packet = self.receive_packet().await?; - - Ok(received_packet.get_body().into()) - } - - async fn receive_multi_packet_response(&mut self) -> Result { - // the server processes packets in order, so send an empty packet and - // remember its id to detect the end of a multi-packet response - let end_id = self.send(PacketType::ExecCommand, "").await?; - - let mut result = String::new(); - - loop { - let received_packet = self.receive_packet().await?; - - if received_packet.get_id() == end_id { - // This is the response to the end-marker packet - return Ok(result); - } - - result += received_packet.get_body(); - } - } - - async fn auth(&mut self, password: &str) -> Result<()> { - self.send(PacketType::Auth, password).await?; - let received_packet = loop { - let received_packet = self.receive_packet().await?; - if received_packet.get_type() == PacketType::AuthResponse { - break received_packet; - } - }; - - if received_packet.is_error() { - Err(Error::Auth) - } else { - Ok(()) - } - } - - async fn send(&mut self, ptype: PacketType, body: &str) -> io::Result { - let id = self.generate_packet_id(); - - let packet = Packet::new(id, ptype, body.into()); - - packet.serialize(&mut self.stream).await?; - - Ok(id) - } - - async fn receive_packet(&mut self) -> io::Result { - Packet::deserialize(&mut self.stream).await - } - - fn generate_packet_id(&mut self) -> i32 { - let id = self.next_packet_id; - - // only use positive ids as the server uses negative ids to signal - // a failed authentication request - self.next_packet_id = self - .next_packet_id - .checked_add(1) - .unwrap_or(INITIAL_PACKET_ID); - - id - } -} - -#[derive(Default, Debug)] -pub struct Builder { - minecraft_quirks_enabled: bool, - factorio_quirks_enabled: bool, -} - -impl Builder { - pub fn new() -> Self { - Self::default() - } - - /// This enables the following quirks for Minecraft: - /// - /// Commands are delayed by 3ms to reduce the chance of crashing the server. - /// See https://bugs.mojang.com/browse/MC-72390 - /// - /// The command length is limited to 1413 bytes. - /// Tests have shown the server to not work reliably - /// with greater command lengths. - pub fn enable_minecraft_quirks(mut self, value: bool) -> Self { - self.minecraft_quirks_enabled = value; - self - } - - /// This enables the following quirks for Factorio: - /// - /// Only single-packet responses are enabled. - /// Multi-packets appear to work differently than in other server implementations - /// (an empty packet gives no response). - pub fn enable_factorio_quirks(mut self, value: bool) -> Self { - self.factorio_quirks_enabled = value; - self - } - - pub async fn connect(self, address: A, password: &str) -> Result - where - A: ToSocketAddrs - { - let stream = TcpStream::connect(address).await?; - let mut conn = Connection { - stream, - next_packet_id: INITIAL_PACKET_ID, - minecraft_quirks_enabled: self.minecraft_quirks_enabled, - factorio_quirks_enabled: self.factorio_quirks_enabled, - }; - - conn.auth(password).await?; - - Ok(conn) - } +pub mod error; +mod connection; + +pub mod battlefield3; +pub mod factorio; +pub mod minecraft; + +/// The prelude should import all the required traits and error related +/// elements, and event in the future macros +pub mod prelude { + pub use super::{ + connection::Connection, + error::Result, + }; } diff --git a/src/minecraft/connection.rs b/src/minecraft/connection.rs new file mode 100644 index 0000000..d9ec103 --- /dev/null +++ b/src/minecraft/connection.rs @@ -0,0 +1,137 @@ +use tokio::net::{TcpStream, ToSocketAddrs}; +use std::io; +use async_trait::async_trait; +use std::time::Duration; + +use crate::connection::Connection; +use crate::error::{Result, Error}; +use crate::packet::{Packet, PacketType}; +use super::packet::MinecraftPacket; + +/// If the size in bytes of the packet is above this the server will reject +/// the connection +const MC_MAX_PAYLOAD_SIZE: usize = 1413; + +/// I dunno if it should be 1 or 0 but `it works` +const INITIAL_PACKET_ID: usize = 1; + +/// It dunno why :P +const DELAY_TIME_MILLIS: u64 = 3; + +/// Representation of the client connection to a Remote Administration Interface +pub struct MinecraftConnection { + stream: TcpStream, + next_packet_id: i32, +} + +impl MinecraftConnection { + /// Auth into the server given the password, login is mandatory + async fn auth(&mut self, password: &str) -> Result<()> { + self.send_packet( + MinecraftPacket::new( + self.next_packet_id, + PacketType::Request(3), + password.to_string(), + ) + ).await?; + + let received_packet = loop { + let received_packet = self.receive_packet().await?; + if let PacketType::Response(2) = received_packet.get_type() { + break received_packet; + } + }; + + if received_packet.is_error() { + Err(Error::Auth) + } else { + Ok(()) + } + } +} + +#[async_trait] +impl Connection for MinecraftConnection { + type Packet = MinecraftPacket; + /// Connects to a rcon server, login is mandatory + async fn connect(address: T, password: &str) -> Result { + let stream = TcpStream::connect(address).await?; + let mut conn = MinecraftConnection { + stream, + next_packet_id: INITIAL_PACKET_ID as i32, + }; + + conn.auth(password).await?; + Ok(conn) + } + + /// Send a certain command, in the implementation the `command` should be + /// parsed to a packet ready bytes buffer and sended via `send_packet` + async fn cmd(&mut self, command: &str) -> Result { + if command.len() >= MC_MAX_PAYLOAD_SIZE { + return Err(Error::CommandTooLong); + } + + self.next_packet_id = self.send_packet( + MinecraftPacket::new( + self.next_packet_id, + PacketType::Request(0), + command.to_owned() + ) + ).await?; + + tokio::time::sleep(Duration::from_millis(DELAY_TIME_MILLIS)).await; + + let response = self.receive_response().await?; + + Ok(response) + } + + /// Receives a response from the rcon server + async fn receive_response(&mut self) -> io::Result { + // The server processes packets in order, so send an empty packet and + // remember its id to detect the end of a multi-packt reponse + let end_id = self.send_packet( + MinecraftPacket::new( + self.next_packet_id, + PacketType::Request(0), + "".to_string(), + ) + ).await?; + + let mut result = String::with_capacity(48); + + loop { + let received_packet = self.receive_packet().await?; + + if received_packet.get_id() == end_id { + // This is the response to the end-marker packet + return Ok(result); + } + + result.push_str(received_packet.get_body()); + } + + } + + /// Low level function that send a Packet, returns the `id` of the sended + /// packet to be incremented + async fn send_packet(&mut self, packet: Self::Packet) -> io::Result { + // I dont know if factorio uses some bits for something in particular + let id = match self.next_packet_id + 1 { + n if n & 0x3fff != 0 => INITIAL_PACKET_ID as i32, + n => n, + }; + + packet.serialize(&mut self.stream).await?; + + Ok(id) + } + + /// Low level function that receives a Packet + async fn receive_packet(&mut self) -> io::Result { + MinecraftPacket::deserialize(&mut self.stream).await + } +} + + diff --git a/src/minecraft/mod.rs b/src/minecraft/mod.rs new file mode 100644 index 0000000..e2fac9c --- /dev/null +++ b/src/minecraft/mod.rs @@ -0,0 +1,4 @@ +mod packet; +mod connection; + +pub use connection::MinecraftConnection; \ No newline at end of file diff --git a/src/minecraft/packet.rs b/src/minecraft/packet.rs new file mode 100644 index 0000000..8675e3a --- /dev/null +++ b/src/minecraft/packet.rs @@ -0,0 +1,120 @@ +use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}; +use async_trait::async_trait; +use std::io; + +use crate::packet::{Packet, PacketType}; + +/// #### Minecraft packets are designed in this way: +/// +/// TODO: ... +/// +#[derive(Debug)] +pub struct MinecraftPacket { + id: i32, + packet_size: i32, + + /// This String must be converted to the required specification at the send + /// time, here the words are separated by spaces + body: String, + + /// The packet type + ptype_n: i32, +} + +impl MinecraftPacket { + /// Instantiates a new MinecraftPacket + pub fn new(id: i32, ptype: PacketType, body: String) -> Self { + // NOTE: This still does not discern the origin of the packet it + // suposses that the reponses always come from the server and the + // requests from the client + + let packet_size = 10 + body.len() as i32; + let ptype_n = match ptype { + PacketType::Request(n) => n, + PacketType::Response(n) => n, + PacketType::Custom(_) => todo!(), + }; + + MinecraftPacket { + id, + packet_size, + body, + ptype_n, + } + } +} + +#[async_trait] +impl Packet for MinecraftPacket { + /// Checks if the packet is an error, probably just useful for Responses + fn is_error(&self) -> bool { + self.id < 0 + } + + /// Serializes de packets, aka convert and send + async fn serialize(&self, w: &mut T) -> io::Result<()> { + let mut buf = Vec::with_capacity(self.packet_size as usize); + + buf.write_i32_le(self.packet_size).await?; + buf.write_i32_le(self.id).await?; + buf.write_i32_le(self.ptype_n).await?; + buf.write_all(self.body.as_bytes()).await?; + buf.write_all(b"\x00\x00").await?; + + w.write_all(&buf).await?; + + Ok(()) + } + + /// Deserializes de packets, aka receive and convert + async fn deserialize(r: &mut T) -> io::Result { + let packet_size = r.read_i32_le().await?; + let id = r.read_i32_le().await?; + let ptype_n = r.read_i32_le().await?; + let body_length = packet_size - 10; + + let mut body = Vec::with_capacity(body_length as usize); + + r.take(body_length as u64) + .read_to_end(&mut body) + .await?; + + let body = String::from_utf8(body) + .map_err(|_| io::Error::from(io::ErrorKind::InvalidData))?; + + // Terminated nulls (\x00\x00) + r.read_u16().await?; + + Ok( + MinecraftPacket { + id, + packet_size, + body, + ptype_n, + } + ) + } + + /// Gets the body of the packet + #[inline] + fn get_body(&self) -> &str { + &self.body + } + + /// Gets the packet type + #[inline] + fn get_type(&self) -> PacketType { + match self.ptype_n { + 0 => PacketType::Response(0), + 2 => PacketType::Request(2), + 3 => PacketType::Request(3), + _ => todo!(), + } + } + + /// Returns the id of the packet and also increments it + #[inline] + fn get_id(&self) -> i32 { + self.id + } +} \ No newline at end of file diff --git a/src/packet.rs b/src/packet.rs index f4e410a..d1c9ae9 100644 --- a/src/packet.rs +++ b/src/packet.rs @@ -8,113 +8,40 @@ // according to those terms. use std::io; -use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}; - +use tokio::io::{AsyncRead, AsyncWrite}; +use async_trait::async_trait; + +/// Representation of the Type of a packet, the number that contains each +/// variant represents some kind of bits into the raw data received or sended +/// with mark the type, in some rcon implementations its just 0 or 1, like in +/// Battlefield but in minecraft the Authorization and AuthorizationResponse are +/// different from normal Requests and Responses so its implementation specific. #[derive(Debug, Clone, Copy, PartialEq)] pub enum PacketType { - Auth, - AuthResponse, - ExecCommand, - ResponseValue, - Unknown(i32), + Request(i32), + Response(i32), + Custom(i32), } -impl PacketType { - fn to_i32(self) -> i32 { - match self { - PacketType::Auth => 3, - PacketType::AuthResponse => 2, - PacketType::ExecCommand => 2, - PacketType::ResponseValue => 0, - PacketType::Unknown(n) => n, - } - } - - pub fn from_i32(n: i32, is_response: bool) -> PacketType { - match n { - 3 => PacketType::Auth, - 2 if is_response => PacketType::AuthResponse, - 2 => PacketType::ExecCommand, - 0 => PacketType::ResponseValue, - n => PacketType::Unknown(n), - } - } -} - -#[derive(Debug)] -pub struct Packet { - length: i32, - id: i32, - ptype: PacketType, - body: String, -} - -impl Packet { - pub fn new(id: i32, ptype: PacketType, body: String) -> Packet { - Packet { - length: 10 + body.len() as i32, - id, - ptype, - body, - } - } - - pub fn is_error(&self) -> bool { - self.id < 0 - } - - pub async fn serialize(&self, w: &mut T) -> io::Result<()> { - // Write bytes to a buffer first so only one tcp packet is sent - // This is done in order to not overwhelm a Minecraft server - let mut buf = Vec::with_capacity(self.length as usize); - - buf.write_i32_le(self.length).await?; - buf.write_i32_le(self.id).await?; - buf.write_i32_le(self.ptype.to_i32()).await?; - buf.write_all(self.body.as_bytes()).await?; - buf.write_all(b"\x00\x00").await?; +/// NOTE: The `#[async_trait]` macro is to be able to declare async functions +/// inside the trait. Maybe in the future is its stabilized remove the +/// dependency +#[async_trait] +pub trait Packet: Sized { + /// Checks if the packet is an error, probably just useful for Responses + fn is_error(&self) -> bool; - w.write_all(&buf).await?; + /// Serializes de packets, aka convert and send + async fn serialize(&self, w: &mut T) -> io::Result<()>; - Ok(()) - } + /// Deserializes de packets, aka receive and convert + async fn deserialize(r: &mut T) -> io::Result; - pub async fn deserialize(r: &mut T) -> io::Result { - let length = r.read_i32_le().await?; - let id = r.read_i32_le().await?; - let ptype = r.read_i32_le().await?; - let body_length = length - 10; - let mut body_buffer = Vec::with_capacity(body_length as usize); + /// Gets the body of the packet + fn get_body(&self) -> &str; + /// Gets the packet type + fn get_type(&self) -> PacketType; - r.take(body_length as u64) - .read_to_end(&mut body_buffer) - .await?; - - let body = String::from_utf8(body_buffer) - .map_err(|_| io::Error::from(io::ErrorKind::InvalidData))?; - - // terminating nulls - r.read_u16().await?; - - let packet = Packet { - length, - id, - ptype: PacketType::from_i32(ptype, true), - body, - }; - - Ok(packet) - } - - pub fn get_body(&self) -> &str { - &self.body - } - - pub fn get_type(&self) -> PacketType { - self.ptype - } - - pub fn get_id(&self) -> i32 { - self.id - } -} + /// Returns the id of the packet and also increments it + fn get_id(&self) -> i32; +} \ No newline at end of file From 986a41dd9170e75db03d242c1aa3d38d23329d46 Mon Sep 17 00:00:00 2001 From: ElGusanitoPLAY Date: Mon, 28 Dec 2020 13:30:25 +0100 Subject: [PATCH 2/2] Minecraft works!! Fixed some packet receive and id stuff --- examples/minecraft.rs | 4 ++-- src/battlefield3/packet.rs | 2 +- src/factorio/connection.rs | 2 +- src/factorio/packet.rs | 3 ++- src/minecraft/connection.rs | 22 +++++++++++----------- src/minecraft/packet.rs | 8 +++----- src/packet.rs | 2 +- 7 files changed, 21 insertions(+), 22 deletions(-) diff --git a/examples/minecraft.rs b/examples/minecraft.rs index c29ca5b..e7f07d4 100644 --- a/examples/minecraft.rs +++ b/examples/minecraft.rs @@ -3,8 +3,8 @@ use rcon::prelude::*; #[tokio::main] async fn main() -> Result<()> { - let address = "127.0.0.1:1234"; - let mut conn = MinecraftConnection::connect(address, "test").await?; + let address = "127.0.0.1:25575"; + let mut conn = MinecraftConnection::connect(address, "root").await?; demo(&mut conn, "list").await?; demo(&mut conn, "say Rust lang rocks! ;P").await?; diff --git a/src/battlefield3/packet.rs b/src/battlefield3/packet.rs index 3c98d54..43de614 100644 --- a/src/battlefield3/packet.rs +++ b/src/battlefield3/packet.rs @@ -149,7 +149,7 @@ impl Packet for Btf3Packet { } /// Gets the packet type #[inline] - fn get_type(&self) -> PacketType { + fn get_type(&self, _is_response: bool) -> PacketType { self.ptype } /// Returns the id of the packet and also increments it diff --git a/src/factorio/connection.rs b/src/factorio/connection.rs index 7df9ece..e4cf3ca 100644 --- a/src/factorio/connection.rs +++ b/src/factorio/connection.rs @@ -34,7 +34,7 @@ impl FactorioConnection { let received_packet = loop { let received_packet = self.receive_packet().await?; - if let PacketType::Response(2) = received_packet.get_type() { + if let PacketType::Response(2) = received_packet.get_type(true) { break received_packet; } }; diff --git a/src/factorio/packet.rs b/src/factorio/packet.rs index 0bedf8c..93db057 100644 --- a/src/factorio/packet.rs +++ b/src/factorio/packet.rs @@ -103,9 +103,10 @@ impl Packet for FactorioPacket { /// Gets the packet type #[inline] - fn get_type(&self) -> PacketType { + fn get_type(&self, is_response: bool) -> PacketType { match self.ptype_n { 0 => PacketType::Response(0), + 2 if is_response => PacketType::Response(2), 2 => PacketType::Request(2), 3 => PacketType::Request(3), _ => todo!(), diff --git a/src/minecraft/connection.rs b/src/minecraft/connection.rs index d9ec103..6cea673 100644 --- a/src/minecraft/connection.rs +++ b/src/minecraft/connection.rs @@ -29,7 +29,7 @@ impl MinecraftConnection { async fn auth(&mut self, password: &str) -> Result<()> { self.send_packet( MinecraftPacket::new( - self.next_packet_id, + 0, PacketType::Request(3), password.to_string(), ) @@ -37,7 +37,7 @@ impl MinecraftConnection { let received_packet = loop { let received_packet = self.receive_packet().await?; - if let PacketType::Response(2) = received_packet.get_type() { + if let PacketType::Response(2) = received_packet.get_type(true) { break received_packet; } }; @@ -58,7 +58,7 @@ impl Connection for MinecraftConnection { let stream = TcpStream::connect(address).await?; let mut conn = MinecraftConnection { stream, - next_packet_id: INITIAL_PACKET_ID as i32, + next_packet_id: 0, }; conn.auth(password).await?; @@ -74,8 +74,8 @@ impl Connection for MinecraftConnection { self.next_packet_id = self.send_packet( MinecraftPacket::new( - self.next_packet_id, - PacketType::Request(0), + 0, + PacketType::Request(2), command.to_owned() ) ).await?; @@ -87,6 +87,7 @@ impl Connection for MinecraftConnection { Ok(response) } + /// Receives a response from the rcon server async fn receive_response(&mut self) -> io::Result { // The server processes packets in order, so send an empty packet and @@ -94,12 +95,12 @@ impl Connection for MinecraftConnection { let end_id = self.send_packet( MinecraftPacket::new( self.next_packet_id, - PacketType::Request(0), + PacketType::Request(2), "".to_string(), ) ).await?; - let mut result = String::with_capacity(48); + let mut result = String::with_capacity(64); loop { let received_packet = self.receive_packet().await?; @@ -111,18 +112,17 @@ impl Connection for MinecraftConnection { result.push_str(received_packet.get_body()); } - } /// Low level function that send a Packet, returns the `id` of the sended /// packet to be incremented - async fn send_packet(&mut self, packet: Self::Packet) -> io::Result { + async fn send_packet(&mut self, mut packet: Self::Packet) -> io::Result { // I dont know if factorio uses some bits for something in particular let id = match self.next_packet_id + 1 { - n if n & 0x3fff != 0 => INITIAL_PACKET_ID as i32, + n if n & 0x3fff == 0 => INITIAL_PACKET_ID as i32, n => n, }; - + packet.id = id; packet.serialize(&mut self.stream).await?; Ok(id) diff --git a/src/minecraft/packet.rs b/src/minecraft/packet.rs index 8675e3a..80070e9 100644 --- a/src/minecraft/packet.rs +++ b/src/minecraft/packet.rs @@ -10,7 +10,7 @@ use crate::packet::{Packet, PacketType}; /// #[derive(Debug)] pub struct MinecraftPacket { - id: i32, + pub id: i32, packet_size: i32, /// This String must be converted to the required specification at the send @@ -54,7 +54,6 @@ impl Packet for MinecraftPacket { /// Serializes de packets, aka convert and send async fn serialize(&self, w: &mut T) -> io::Result<()> { let mut buf = Vec::with_capacity(self.packet_size as usize); - buf.write_i32_le(self.packet_size).await?; buf.write_i32_le(self.id).await?; buf.write_i32_le(self.ptype_n).await?; @@ -78,7 +77,6 @@ impl Packet for MinecraftPacket { r.take(body_length as u64) .read_to_end(&mut body) .await?; - let body = String::from_utf8(body) .map_err(|_| io::Error::from(io::ErrorKind::InvalidData))?; @@ -103,11 +101,11 @@ impl Packet for MinecraftPacket { /// Gets the packet type #[inline] - fn get_type(&self) -> PacketType { + fn get_type(&self, is_response: bool) -> PacketType { match self.ptype_n { 0 => PacketType::Response(0), + 2 if is_response => PacketType::Response(2), 2 => PacketType::Request(2), - 3 => PacketType::Request(3), _ => todo!(), } } diff --git a/src/packet.rs b/src/packet.rs index d1c9ae9..64b9e3a 100644 --- a/src/packet.rs +++ b/src/packet.rs @@ -40,7 +40,7 @@ pub trait Packet: Sized { /// Gets the body of the packet fn get_body(&self) -> &str; /// Gets the packet type - fn get_type(&self) -> PacketType; + fn get_type(&self, is_response: bool) -> PacketType; /// Returns the id of the packet and also increments it fn get_id(&self) -> i32;