From 717a2b77859c18e0176b0105254c861754524ffe Mon Sep 17 00:00:00 2001 From: Joshua Goins Date: Tue, 15 Apr 2025 23:35:49 -0400 Subject: [PATCH] Add RCON support Just for fun, but this isn't hooked up to any commands yet. I need to make some command changes anyway, and will hook it up when I do that refactoring. --- Cargo.lock | 6 +++ Cargo.toml | 3 ++ src/bin/kawari-world.rs | 112 +++++++++++++++++++++++++++++++--------- src/config.rs | 54 +++++++++++++++++-- 4 files changed, 147 insertions(+), 28 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c073c57..4a12533 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -737,6 +737,7 @@ dependencies = [ "physis", "rand", "reqwest", + "rkon", "rusqlite", "serde", "serde_json", @@ -1126,6 +1127,11 @@ dependencies = [ "windows-registry", ] +[[package]] +name = "rkon" +version = "0.1.0" +source = "git+https://codeberg.org/redstrate/rkon#23b998a070d2f8d626e0a19d48232e9dde641533" + [[package]] name = "rusqlite" version = "0.34.0" diff --git a/Cargo.toml b/Cargo.toml index d438749..5cc23e4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -94,3 +94,6 @@ zip = { version = "2.6", features = ["deflate", "lzma", "bzip2"], default-featur # For some login<->lobby server communication reqwest = { version = "0.12", default-features = false } + +# For RCON +rkon = { git = "https://codeberg.org/redstrate/rkon" } diff --git a/src/bin/kawari-world.rs b/src/bin/kawari-world.rs index e1e7954..673ce39 100644 --- a/src/bin/kawari-world.rs +++ b/src/bin/kawari-world.rs @@ -30,7 +30,7 @@ use kawari::world::{ }; use mlua::{Function, Lua}; -use tokio::io::AsyncReadExt; +use tokio::io::{AsyncReadExt, AsyncWriteExt}; use tokio::join; use tokio::net::TcpListener; use tokio::sync::mpsc::{Receiver, UnboundedReceiver, UnboundedSender, channel, unbounded_channel}; @@ -1032,6 +1032,16 @@ async fn main() { let listener = TcpListener::bind(addr).await.unwrap(); + let rcon_listener = if !config.world.rcon_password.is_empty() { + Some( + TcpListener::bind(config.world.get_rcon_socketaddr()) + .await + .unwrap(), + ) + } else { + None + }; + tracing::info!("Server started on {addr}"); let database = Arc::new(WorldDatabase::new()); @@ -1072,30 +1082,84 @@ async fn main() { let (handle, _) = spawn_main_loop(); loop { - let (socket, ip) = listener.accept().await.unwrap(); - let id = handle.next_id(); + tokio::select! { + Ok((socket, ip)) = listener.accept() => { + let id = handle.next_id(); - let state = PacketState { - client_key: None, - clientbound_oodle: OodleNetwork::new(), - serverbound_oodle: OodleNetwork::new(), + let state = PacketState { + client_key: None, + clientbound_oodle: OodleNetwork::new(), + serverbound_oodle: OodleNetwork::new(), + }; + + spawn_client(ZoneConnection { + socket, + state, + player_data: PlayerData::default(), + spawn_index: 0, + zone: None, + status_effects: StatusEffects::default(), + event: None, + actors: Vec::new(), + ip, + id, + handle: handle.clone(), + database: database.clone(), + lua: lua.clone(), + gamedata: game_data.clone(), + }); + } + Ok((mut socket, _)) = rcon_listener.as_ref().unwrap().accept(), if rcon_listener.is_some() => { + let mut authenticated = false; + + loop { + // read from client + let mut resp_bytes = [0u8; rkon::MAX_PACKET_SIZE]; + let n = socket.read(&mut resp_bytes).await.unwrap(); + if n > 0 { + let request = rkon::Packet::decode(&resp_bytes).unwrap(); + + match request.packet_type { + rkon::PacketType::Command => { + if authenticated { + let response = rkon::Packet { + request_id: request.request_id, + packet_type: rkon::PacketType::Command, + body: "hello world!".to_string() + }; + let encoded = response.encode(); + socket.write_all(&encoded).await.unwrap(); + } + }, + rkon::PacketType::Login => { + let config = get_config(); + if request.body == config.world.rcon_password { + authenticated = true; + + let response = rkon::Packet { + request_id: request.request_id, + packet_type: rkon::PacketType::Command, + body: String::default() + }; + let encoded = response.encode(); + socket.write_all(&encoded).await.unwrap(); + } else { + authenticated = false; + + let response = rkon::Packet { + request_id: -1, + packet_type: rkon::PacketType::Command, + body: String::default() + }; + let encoded = response.encode(); + socket.write_all(&encoded).await.unwrap(); + } + }, + _ => tracing::warn!("Ignoring unknown RCON packet") + } + } + } + } }; - - spawn_client(ZoneConnection { - socket, - state, - player_data: PlayerData::default(), - spawn_index: 0, - zone: None, - status_effects: StatusEffects::default(), - event: None, - actors: Vec::new(), - ip, - id, - handle: handle.clone(), - database: database.clone(), - lua: lua.clone(), - gamedata: game_data.clone(), - }); } } diff --git a/src/config.rs b/src/config.rs index 0d4bb3a..e140b03 100644 --- a/src/config.rs +++ b/src/config.rs @@ -187,26 +187,64 @@ impl WebConfig { /// Configuration for the world server. #[derive(Serialize, Deserialize)] pub struct WorldConfig { + #[serde(default = "WorldConfig::default_port")] pub port: u16, + #[serde(default = "WorldConfig::default_listen_address")] pub listen_address: String, /// See the World Excel sheet. + #[serde(default = "WorldConfig::default_world_id")] pub world_id: u16, /// Location of the scripts directory. /// Defaults to a sensible value if the project is self-built. + #[serde(default = "WorldConfig::default_scripts_location")] pub scripts_location: String, + /// Port of the RCON server. + #[serde(default = "WorldConfig::default_rcon_port")] + pub rcon_port: u16, + /// Password of the RCON server, if left blank (the default) RCON is disabled. + #[serde(default = "WorldConfig::default_rcon_password")] + pub rcon_password: String, } impl Default for WorldConfig { fn default() -> Self { Self { - port: 7100, - listen_address: "127.0.0.1".to_string(), - world_id: 63, // Gilgamesh - scripts_location: "resources/scripts".to_string(), + port: Self::default_port(), + listen_address: Self::default_listen_address(), + world_id: Self::default_world_id(), + scripts_location: Self::default_scripts_location(), + rcon_port: Self::default_rcon_port(), + rcon_password: Self::default_rcon_password(), } } } +impl WorldConfig { + fn default_port() -> u16 { + 7100 + } + + fn default_listen_address() -> String { + "127.0.0.1".to_string() + } + + fn default_world_id() -> u16 { + 63 // Gilgamesh + } + + fn default_scripts_location() -> String { + "resources/scripts".to_string() + } + + fn default_rcon_port() -> u16 { + 25575 + } + + fn default_rcon_password() -> String { + String::default() + } +} + impl WorldConfig { /// Returns the configured IP address & port as a `SocketAddr`. pub fn get_socketaddr(&self) -> SocketAddr { @@ -215,6 +253,14 @@ impl WorldConfig { self.port, )) } + + /// Returns the configured IP address & port as a `SocketAddr` for RCON. + pub fn get_rcon_socketaddr(&self) -> SocketAddr { + SocketAddr::from(( + IpAddr::from_str(&self.listen_address).expect("Invalid IP address format in config!"), + self.rcon_port, + )) + } } /// Global and all-encompassing config.