Added RCON support

This commit is contained in:
Snowiiii
2024-08-10 23:53:32 +02:00
parent 50dcbfeb6d
commit 414cf3190c
12 changed files with 386 additions and 16 deletions

1
Cargo.lock generated
View File

@@ -1154,6 +1154,7 @@ name = "pumpkin"
version = "0.1.0-dev"
dependencies = [
"base64",
"bytes",
"crossbeam-channel",
"digest 0.11.0-pre.9",
"image 0.25.2",

View File

@@ -4,7 +4,7 @@
![CI](https://github.com/Snowiiii/Pumpkin/actions/workflows/rust.yml/badge.svg)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
![Current version)](https://img.shields.io/badge/current_version-1.21-blue)
![Current version)](https://img.shields.io/badge/current_version-1.21.1-blue)
</div>
@@ -27,13 +27,10 @@ Pumpkin is currently under heavy development.
### Features (WIP)
- [x] Configuration (toml)
- [x] Server Status/Ping
- [x] Custom maximum player amout
- [x] Custom maximum player amount
- [x] Custom Icon
- [x] Custom Status (MOTD)
- Login
- [x] Authentication
- [x] Encryption
- [x] Packet Compression
- [x] Login
- Player Configuration
- [x] Registries (biome types, paintings, dimensions)
- [x] Server Brand
@@ -57,6 +54,7 @@ Pumpkin is currently under heavy development.
- [ ] Player Inventory
- [ ] Player Attack
- Server
- [x] RCON
- [x] Inventories
- [x] Chat
- [x] Commands

View File

@@ -2,6 +2,6 @@ pub struct ChatType {}
pub struct Decoration {
translation_key: String,
// style: Option,
// style: Option,
parameters: Vec<String>,
}

View File

@@ -231,4 +231,4 @@ impl Default for TextContent {
fn default() -> Self {
Self::Text { text: "".into() }
}
}
}

View File

@@ -17,6 +17,8 @@ serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
toml = "0.8.19"
bytes = "1.7"
rand = "0.8.5"
num-traits = "0.2"

View File

@@ -1,9 +1,9 @@
use std::{collections::HashMap, net::IpAddr, time::Duration};
use std::{collections::HashMap, net::IpAddr};
use base64::{engine::general_purpose, Engine};
use num_bigint::BigInt;
use pumpkin_protocol::Property;
use reqwest::{header::CONTENT_TYPE, StatusCode, Url};
use reqwest::{StatusCode, Url};
use serde::{Deserialize, Serialize};
use thiserror::Error;
use uuid::Uuid;

View File

@@ -8,8 +8,9 @@ use pumpkin_protocol::{
server::play::{
SChatCommand, SChatMessage, SConfirmTeleport, SPlayerCommand, SPlayerPosition,
SPlayerPositionRotation, SPlayerRotation, SSwingArm,
}, VarInt,
};
},
VarInt,
};
use pumpkin_text::TextComponent;
use crate::{
@@ -156,7 +157,7 @@ impl Client {
}
pub fn handle_chat_command(&mut self, _server: &mut Server, command: SChatCommand) {
handle_command(&mut CommandSender::Player(self), command.command);
handle_command(&mut CommandSender::Player(self), &command.command);
}
pub fn handle_player_command(&mut self, _server: &mut Server, command: SPlayerCommand) {

View File

@@ -23,6 +23,7 @@ pub trait Command<'a> {
}
pub enum CommandSender<'a> {
Rcon(&'a mut Vec<String>),
Console,
Player(&'a mut Client),
}
@@ -33,6 +34,7 @@ impl<'a> CommandSender<'a> {
// TODO: add color and stuff to console
CommandSender::Console => log::info!("{:?}", text.content),
CommandSender::Player(c) => c.send_system_message(text),
CommandSender::Rcon(s) => s.push(format!("{:?}", text.content)),
}
}
@@ -40,6 +42,7 @@ impl<'a> CommandSender<'a> {
match self {
CommandSender::Console => false,
CommandSender::Player(_) => true,
CommandSender::Rcon(_) => false,
}
}
@@ -47,16 +50,18 @@ impl<'a> CommandSender<'a> {
match self {
CommandSender::Console => true,
CommandSender::Player(_) => false,
CommandSender::Rcon(_) => true,
}
}
pub fn as_mut_player(&mut self) -> Option<&mut Client> {
match self {
CommandSender::Player(client) => Some(client),
CommandSender::Console => None,
CommandSender::Rcon(_) => None,
}
}
}
pub fn handle_command(sender: &mut CommandSender, command: String) {
pub fn handle_command(sender: &mut CommandSender, command: &str) {
let command = command.to_lowercase();
// an ugly mess i know
if command.starts_with(PumpkinCommand::NAME) {

View File

@@ -21,6 +21,26 @@ pub struct AdvancedConfiguration {
pub authentication: Authentication,
pub packet_compression: Compression,
pub resource_pack: ResourcePack,
pub rcon: RCONConfig,
}
#[derive(Deserialize, Serialize, Clone)]
pub struct RCONConfig {
pub enabled: bool,
pub ip: String,
pub port: u16,
pub password: String,
}
impl Default for RCONConfig {
fn default() -> Self {
Self {
enabled: false,
ip: "0.0.0.0".to_string(),
port: 25575,
password: "".to_string(),
}
}
}
#[derive(Deserialize, Serialize)]
@@ -67,6 +87,7 @@ impl Default for AdvancedConfiguration {
commands: Commands::default(),
packet_compression: Compression::default(),
resource_pack: ResourcePack::default(),
rcon: RCONConfig::default(),
}
}
}
@@ -107,7 +128,7 @@ impl Default for BasicConfiguration {
fn default() -> Self {
Self {
config_version: CURRENT_BASE_VERSION.to_string(),
server_address: "127.0.0.1".to_string(),
server_address: "0.0.0.0".to_string(),
server_port: 25565,
seed: "".to_string(),
max_players: 100000,

View File

@@ -19,6 +19,7 @@ pub mod client;
pub mod commands;
pub mod config;
pub mod entity;
pub mod rcon;
pub mod server;
pub mod util;
@@ -27,6 +28,8 @@ pub mod util;
async fn main() -> io::Result<()> {
use std::{cell::RefCell, time::Instant};
use rcon::RCONServer;
let time = Instant::now();
let basic_config = BasicConfiguration::load("configuration.toml");
@@ -58,6 +61,7 @@ async fn main() -> io::Result<()> {
let mut unique_token = Token(SERVER.0 + 1);
let use_console = advanced_configuration.commands.use_console;
let rcon = advanced_configuration.rcon.clone();
let mut connections: HashMap<Token, Rc<RefCell<Client>>> = HashMap::new();
@@ -73,10 +77,15 @@ async fn main() -> io::Result<()> {
stdin
.read_line(&mut out)
.expect("Failed to read console line");
handle_command(&mut commands::CommandSender::Console, out);
handle_command(&mut commands::CommandSender::Console, &out);
}
});
}
if rcon.enabled {
tokio::spawn(async move {
RCONServer::new(&rcon).await.unwrap();
});
}
loop {
if let Err(err) = poll.poll(&mut events, None) {
if interrupted(&err) {

220
pumpkin/src/rcon/mod.rs Normal file
View File

@@ -0,0 +1,220 @@
use std::{
collections::HashMap,
io::{self, Read},
};
use mio::{
net::{TcpListener, TcpStream},
Events, Interest, Poll, Token,
};
use packet::{Packet, PacketError, PacketType};
use thiserror::Error;
use crate::{commands::handle_command, config::RCONConfig};
mod packet;
#[derive(Debug, Error)]
pub enum RCONError {
#[error("authentication failed")]
Auth,
#[error("command exceeds the maximum length")]
CommandTooLong,
#[error("{}", _0)]
Io(io::Error),
}
const SERVER: Token = Token(0);
pub struct RCONServer {}
impl RCONServer {
pub async fn new(config: &RCONConfig) -> Result<Self, io::Error> {
assert!(config.enabled, "RCON is not enabled");
let addr = format!("{}:{}", config.ip, config.port)
.parse()
.expect("Failed to parse RCON address");
let mut poll = Poll::new().unwrap();
let mut listener = TcpListener::bind(addr).unwrap();
poll.registry()
.register(&mut listener, SERVER, Interest::READABLE)
.unwrap();
let mut unique_token = Token(SERVER.0 + 1);
let mut events = Events::with_capacity(128);
let mut connections: HashMap<Token, RCONClient> = HashMap::new();
let password = config.password.clone();
loop {
poll.poll(&mut events, None).unwrap();
for event in events.iter() {
match event.token() {
SERVER => loop {
// Received an event for the TCP server socket, which
// indicates we can accept an connection.
let (mut connection, address) = match listener.accept() {
Ok((connection, address)) => (connection, address),
Err(e) if e.kind() == io::ErrorKind::WouldBlock => {
// If we get a `WouldBlock` error we know our
// listener has no more incoming connections queued,
// so we can return to polling and wait for some
// more.
break;
}
Err(e) => {
// If it was any other kind of error, something went
// wrong and we terminate with an error.
return Err(e);
}
};
log::info!("Accepted connection from: {}", address);
let token = Self::next(&mut unique_token);
poll.registry()
.register(
&mut connection,
token,
Interest::READABLE.add(Interest::WRITABLE),
)
.unwrap();
connections.insert(token, RCONClient::new(connection));
},
token => {
let done = if let Some(client) = connections.get_mut(&token) {
client.handle(&password).await
} else {
false
};
if done {
if let Some(mut client) = connections.remove(&token) {
poll.registry().deregister(&mut client.connection)?;
}
}
}
}
}
}
}
fn next(current: &mut Token) -> Token {
let next = current.0;
current.0 += 1;
Token(next)
}
}
pub struct RCONClient {
connection: TcpStream,
logged_in: bool,
incoming: Vec<u8>,
closed: bool,
}
impl RCONClient {
pub fn new(connection: TcpStream) -> Self {
Self {
connection,
logged_in: false,
incoming: Vec::new(),
closed: false,
}
}
pub async fn handle(&mut self, password: &str) -> bool {
if !self.closed {
loop {
match self.read_bytes() {
// Stream closed, so we can't reply, so we just close everything.
Ok(true) => return true,
Ok(false) => {}
Err(e) if e.kind() == io::ErrorKind::WouldBlock => break,
Err(e) => {
log::error!("could not read packet: {e}");
return true;
}
}
}
// If we get a close here, we might have a reply, which we still want to write.
match self.poll(password).await {
Ok(()) => {}
Err(e) => {
log::error!("rcon error: {e}");
self.closed = true;
}
}
}
self.closed
}
async fn poll(&mut self, password: &str) -> Result<(), PacketError> {
loop {
let packet = match self.receive_packet().await? {
Some(p) => p,
None => return Ok(()),
};
match packet.get_type() {
PacketType::Auth => {
let body = packet.get_body();
if !body.is_empty() && packet.get_body() == password {
self.send(&mut Packet::new(
packet.get_id(),
PacketType::AuthResponse,
"".into(),
))
.await
.unwrap();
log::info!("RCON Client logged in successfully");
self.logged_in = true;
} else {
log::warn!("RCON Client has tried wrong password");
self.send(&mut Packet::new(-1, PacketType::AuthResponse, "".into()))
.await
.unwrap();
return Err(PacketError::WrongPassword);
}
}
PacketType::ExecCommand => {
if self.logged_in {
let mut output = Vec::new();
handle_command(
&mut crate::commands::CommandSender::Rcon(&mut output),
packet.get_body(),
);
for line in output {
self.send(&mut Packet::new(packet.get_id(), PacketType::Output, line))
.await
.unwrap();
}
}
}
PacketType::Output => todo!(),
PacketType::AuthResponse => unreachable!(),
}
}
}
fn read_bytes(&mut self) -> io::Result<bool> {
let mut buf = [0; 1460];
let n = self.connection.read(&mut buf)?;
if n == 0 {
return Ok(true);
}
self.incoming.extend_from_slice(&buf[..n]);
Ok(false)
}
async fn send(&mut self, packet: &mut Packet) -> io::Result<()> {
packet.send_packet(&mut self.connection).await
}
async fn receive_packet(&mut self) -> Result<Option<Packet>, PacketError> {
Packet::deserialize(&mut self.incoming).await
}
}

113
pumpkin/src/rcon/packet.rs Normal file
View File

@@ -0,0 +1,113 @@
use std::io::{self, BufRead, Cursor, Write};
use bytes::{BufMut, BytesMut};
use mio::net::TcpStream;
use thiserror::Error;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum PacketType {
Auth,
AuthResponse,
ExecCommand,
Output,
}
#[derive(Error, Debug)]
pub enum PacketError {
#[error("invalid length")]
InvalidLength,
#[error("expected terminating NUL byte")]
NoNullTermination,
#[error("wrong password")]
WrongPassword,
}
impl PacketType {
fn to_i32(self) -> i32 {
match self {
PacketType::Auth => 3,
PacketType::AuthResponse => 2,
PacketType::ExecCommand => 2,
PacketType::Output => 0,
}
}
pub fn from_i32(n: i32) -> PacketType {
match n {
3 => PacketType::Auth,
2 => PacketType::ExecCommand,
_ => PacketType::Output,
}
}
}
#[derive(Debug)]
pub struct Packet {
id: i32,
ptype: PacketType,
body: String,
}
impl Packet {
pub fn new(id: i32, ptype: PacketType, body: String) -> Packet {
Packet { id, ptype, body }
}
pub async fn send_packet(&mut self, connection: &mut TcpStream) -> io::Result<()> {
// let len = outgoing.len() as u64;
let mut buf = BytesMut::new();
// 10 is for 4 bytes ty, 4 bytes id, and 2 terminating nul bytes.
buf.put_i32_le(10 + self.get_body().len() as i32);
buf.put_i32_le(self.id);
buf.put_i32_le(self.get_type().to_i32());
let bytes = self.get_body().as_bytes();
buf.put_slice(bytes);
buf.put_u8(0);
buf.put_u8(0);
connection.write(&buf).unwrap();
Ok(())
}
pub async fn deserialize(incoming: &mut Vec<u8>) -> Result<Option<Packet>, PacketError> {
if incoming.len() < 4 {
return Ok(None);
}
let mut buf = Cursor::new(&incoming);
let len = buf.read_i32_le().await.unwrap() + 4;
if !(0..=1460).contains(&len) {
return Err(PacketError::InvalidLength);
}
let id = buf.read_i32_le().await.unwrap();
let ty = buf.read_i32_le().await.unwrap();
let mut payload = vec![];
let _ = buf.read_until(b'\0', &mut payload).unwrap();
payload.pop();
if buf.read_u8().await.unwrap() != 0 {
return Err(PacketError::NoNullTermination);
}
if buf.position() != len as u64 {
return Err(PacketError::InvalidLength);
}
incoming.drain(0..len as usize);
let packet = Packet {
id,
ptype: PacketType::from_i32(ty),
body: String::from_utf8(payload).unwrap(),
};
Ok(Some(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
}
}