Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added Battlefield 3 support and restructurized how the Connection and Packets work #16

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"] }

Expand Down
19 changes: 19 additions & 0 deletions examples/btf3login.rs
Original file line number Diff line number Diff line change
@@ -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(())
}
16 changes: 7 additions & 9 deletions examples/factorio.rs
Original file line number Diff line number Diff line change
@@ -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(())
}
}
33 changes: 9 additions & 24 deletions examples/minecraft.rs
Original file line number Diff line number Diff line change
@@ -1,35 +1,20 @@
// Copyright (c) 2015 [rust-rcon developers]
// Licensed under the Apache License, Version 2.0
// <LICENSE-APACHE or
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT
// license <LICENSE-MIT or http://opensource.org/licenses/MIT>,
// 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:25575";
let mut conn = MinecraftConnection::connect(address, "root").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(())
}
}
101 changes: 101 additions & 0 deletions src/battlefield3/connection.rs
Original file line number Diff line number Diff line change
@@ -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<bool> {
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<T: ToSocketAddrs + Send>(address: T, password: &str) -> Result<Self> {
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<String> {
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<String> {
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<i32> {
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<Self::Packet> {
Btf3Packet::deserialize(&mut self.stream).await
}
}

4 changes: 4 additions & 0 deletions src/battlefield3/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
mod connection;
mod packet;

pub use connection::Btf3Connection;
162 changes: 162 additions & 0 deletions src/battlefield3/packet.rs
Original file line number Diff line number Diff line change
@@ -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<T: Unpin + AsyncWrite + Send>(&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<T: Unpin + AsyncRead + Send>(r: &mut T) -> io::Result<Self> {
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, _is_response: bool) -> 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 {}
Loading