mirror of
https://github.com/Pumpkin-MC/Pumpkin.git
synced 2026-08-31 20:32:34 +00:00
Added RCON support
This commit is contained in:
1
Cargo.lock
generated
1
Cargo.lock
generated
@@ -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",
|
||||
|
||||
10
README.md
10
README.md
@@ -4,7 +4,7 @@
|
||||
|
||||

|
||||
[](https://opensource.org/licenses/MIT)
|
||||

|
||||

|
||||
|
||||
</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
|
||||
|
||||
@@ -2,6 +2,6 @@ pub struct ChatType {}
|
||||
|
||||
pub struct Decoration {
|
||||
translation_key: String,
|
||||
// style: Option,
|
||||
// style: Option,
|
||||
parameters: Vec<String>,
|
||||
}
|
||||
|
||||
@@ -231,4 +231,4 @@ impl Default for TextContent {
|
||||
fn default() -> Self {
|
||||
Self::Text { text: "".into() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
220
pumpkin/src/rcon/mod.rs
Normal 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
113
pumpkin/src/rcon/packet.rs
Normal 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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user