#![cfg_attr(doc_cfg, feature(doc_cfg))]
use err_derive::Error;
use packet::{Packet, PacketType};
use std::fmt::{self, Debug, Formatter};
use std::future::Future;
use std::io;
use std::marker::PhantomData;
use std::pin::Pin;
use std::sync::Arc;
use std::time::Duration;
use tokio::io::{AsyncRead, AsyncWrite};
#[cfg(feature = "rt-async-std")]
mod rt_async_std;
#[cfg(feature = "rt-async-std")]
pub use rt_async_std::AsyncStdStream;
#[cfg(feature = "rt-tokio")]
mod rt_tokio;
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<T> = std::result::Result<T, Error>;
pub struct Connection<T> {
io: T,
next_packet_id: i32,
minecraft_quirks_enabled: bool,
factorio_quirks_enabled: bool,
sleep_fn: SleepFn,
}
impl<T: AsyncRead + AsyncWrite + Unpin> Connection<T> {
pub fn builder() -> Builder<T> {
Builder::new()
}
#[cfg(any(feature = "rt-tokio", feature = "rt-async-std"))]
#[cfg_attr(doc_cfg, doc(cfg(any(feature = "rt-tokio", feature = "rt-async-std"))))]
pub async fn handshake(io: T, password: &str) -> Result<Self> {
Self::builder()
.enable_minecraft_quirks(true)
.handshake(io, password)
.await
}
pub async fn cmd(&mut self, cmd: &str) -> Result<String> {
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 {
self.sleep_fn
.call(Duration::from_millis(DELAY_TIME_MILLIS))
.await;
}
let response = self.receive_response().await?;
Ok(response)
}
async fn receive_response(&mut self) -> Result<String> {
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<String> {
let received_packet = self.receive_packet().await?;
Ok(received_packet.get_body().into())
}
async fn receive_multi_packet_response(&mut self) -> Result<String> {
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 {
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<i32> {
let id = self.generate_packet_id();
let packet = Packet::new(id, ptype, body.into());
packet.serialize(&mut self.io).await?;
Ok(id)
}
async fn receive_packet(&mut self) -> io::Result<Packet> {
Packet::deserialize(&mut self.io).await
}
fn generate_packet_id(&mut self) -> i32 {
let id = self.next_packet_id;
self.next_packet_id = self
.next_packet_id
.checked_add(1)
.unwrap_or(INITIAL_PACKET_ID);
id
}
}
#[derive(Clone)]
enum SleepFn {
#[cfg(feature = "rt-tokio")]
Tokio,
#[cfg(feature = "rt-async-std")]
AsyncStd,
#[cfg(not(any(feature = "rt-tokio", feature = "rt-async-std")))]
None,
Custom(CustomSleepFn),
}
impl Debug for SleepFn {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match self {
#[cfg(feature = "rt-tokio")]
Self::Tokio => f.write_str("tokio::time::sleep"),
#[cfg(feature = "rt-async-std")]
Self::AsyncStd => f.write_str("async_std::task::sleep"),
#[cfg(not(any(feature = "rt-tokio", feature = "rt-async-std")))]
Self::None => f.write_str("None"),
Self::Custom(_) => f.write_str("custom sleep function"),
}
}
}
type CustomSleepFn =
Arc<dyn Fn(Duration) -> Pin<Box<dyn Future<Output = ()> + Send>> + Send + Sync>;
impl SleepFn {
async fn call(&mut self, duration: Duration) {
match self {
#[cfg(feature = "rt-tokio")]
Self::Tokio => tokio::time::sleep(duration).await,
#[cfg(feature = "rt-async-std")]
Self::AsyncStd => async_std::task::sleep(duration).await,
#[cfg(not(any(feature = "rt-tokio", feature = "rt-async-std")))]
Self::None => unreachable!(),
Self::Custom(f) => f(duration).await,
}
}
}
#[derive(Debug)]
pub struct Builder<T> {
minecraft_quirks_enabled: bool,
factorio_quirks_enabled: bool,
sleep_fn: SleepFn,
_io: PhantomData<fn() -> T>,
}
impl<T> Default for Builder<T> {
fn default() -> Self {
#[cfg(feature = "rt-tokio")]
let sleep_fn = SleepFn::Tokio;
#[cfg(all(feature = "rt-async-std", not(feature = "rt-tokio")))]
let sleep_fn = SleepFn::AsyncStd;
#[cfg(not(any(feature = "rt-async-std", feature = "rt-tokio")))]
let sleep_fn = SleepFn::None;
Self {
minecraft_quirks_enabled: false,
factorio_quirks_enabled: false,
sleep_fn,
_io: PhantomData,
}
}
}
impl<T> Clone for Builder<T> {
fn clone(&self) -> Self {
Self {
minecraft_quirks_enabled: self.minecraft_quirks_enabled,
factorio_quirks_enabled: self.factorio_quirks_enabled,
sleep_fn: self.sleep_fn.clone(),
_io: PhantomData,
}
}
}
impl<T> Builder<T> {
pub fn new() -> Self {
Self::default()
}
pub fn enable_minecraft_quirks(mut self, value: bool) -> Self {
self.minecraft_quirks_enabled = value;
self
}
pub fn enable_factorio_quirks(mut self, value: bool) -> Self {
self.factorio_quirks_enabled = value;
self
}
pub fn sleep_fn<F, Fut>(mut self, f: F) -> Self
where
F: Fn(Duration) -> Fut + Send + Sync + 'static,
Fut: Future<Output = ()> + Send + 'static,
{
self.sleep_fn = SleepFn::Custom(Arc::new(move |duration| Box::pin(f(duration))));
self
}
pub async fn handshake(self, io: T, password: &str) -> Result<Connection<T>>
where
T: AsyncRead + AsyncWrite + Unpin,
{
#[cfg(not(any(feature = "rt-tokio", feature = "rt-async-std")))]
if self.minecraft_quirks_enabled && matches!(self.sleep_fn, SleepFn::None) {
panic!(
"\
Minecraft quirks mode is enabled, but no runtime or custom sleep function has been \
set. Enable one of the `rt-tokio` or `rt-async-std` feature flags, or set a custom \
sleep function with `rcon::Builder::sleep_fn`.\
"
);
}
let mut conn = Connection {
io,
next_packet_id: INITIAL_PACKET_ID,
minecraft_quirks_enabled: self.minecraft_quirks_enabled,
factorio_quirks_enabled: self.factorio_quirks_enabled,
sleep_fn: self.sleep_fn,
};
conn.auth(password).await?;
Ok(conn)
}
}