diff --git a/.gitignore b/.gitignore index 22c1f85..5761367 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ .env +lazymc.toml /target # Test server diff --git a/Cargo.lock b/Cargo.lock index 561b7db..fe87c79 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -216,7 +216,9 @@ dependencies = [ "log", "minecraft-protocol", "pretty_env_logger", + "serde", "tokio", + "toml", ] [[package]] @@ -611,6 +613,15 @@ dependencies = [ "syn", ] +[[package]] +name = "toml" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31142970826733df8241ef35dc040ef98c679ab14d7c3e54d827099b3acecaa" +dependencies = [ + "serde", +] + [[package]] name = "unicode-xid" version = "0.2.2" diff --git a/Cargo.toml b/Cargo.toml index 9dbd9b0..1c42d4d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,3 +28,6 @@ tokio = { version = "1", default-features = false, features = ["rt", "rt-multi-t dotenv = "0.15" log = "0.4" pretty_env_logger = "0.4" + +serde = "1.0" +toml = "0.5" diff --git a/src/config.rs b/src/config.rs index 0b48e4f..8e96472 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,21 +1,81 @@ -/// Command to start server. -pub const SERVER_CMD: &str = "/home/timvisee/git/lazymc/mcserver/start"; +use std::fs; +use std::io; +use std::net::SocketAddr; +use std::path::PathBuf; -/// Public address for users to connect to. -pub const ADDRESS_PUBLIC: &str = "127.0.0.1:9090"; +use serde::Deserialize; -/// Minecraft server address to proxy to. -pub const ADDRESS_PROXY: &str = "127.0.0.1:9091"; +/// Default configuration file location. +const CONFIG_FILE: &str = "lazymc.toml"; -/// Server description shown when server is starting. -pub const LABEL_SERVER_SLEEPING: &str = "☠ Server is sleeping\n§2☻ Join to start it up"; +/// Configuration. +#[derive(Debug, Deserialize)] +pub struct Config { + /// Public configuration. + pub public: Public, -/// Server description shown when server is starting. -pub const LABEL_SERVER_STARTING: &str = "§2☻ Server is starting...\n§7⌛ Please wait..."; + /// Server configuration. + pub server: Server, -/// Kick message shown when user tries to connect to starting server. -pub const LABEL_SERVER_STARTING_MESSAGE: &str = - "Server is starting... §c♥§r\n\nThis may take some time.\n\nPlease try to reconnect in a minute."; + /// Time configuration. + pub time: Time, -/// Idle server sleeping delay in seconds. -pub const SLEEP_DELAY: u64 = 10; + /// Messages, shown to the user. + pub messages: Messages, +} + +impl Config { + /// Load configuration form file. + pub fn load() -> Result { + let data = fs::read(CONFIG_FILE)?; + let config = toml::from_slice(&data)?; + Ok(config) + } +} + +/// Public configuration. +#[derive(Debug, Deserialize)] +pub struct Public { + /// Egress address. + #[serde(alias = "address_egress")] + pub address: SocketAddr, +} + +/// Server configuration. +#[derive(Debug, Deserialize)] +pub struct Server { + /// Server directory. + pub directory: PathBuf, + + /// Start command. + pub command: String, + + /// Ingress address. + #[serde(alias = "address_ingress")] + pub address: SocketAddr, +} + +/// Time configuration. +#[derive(Debug, Deserialize)] +pub struct Time { + /// Sleep after number of seconds. + pub sleep_after: u32, + + /// Minimum time in seconds to stay online when server is started. + // TODO: implement this + #[serde(alias = "minimum_online_time")] + pub min_online_time: u32, +} + +/// Messages. +#[derive(Debug, Deserialize)] +pub struct Messages { + /// MOTD when server is sleeping. + pub motd_sleeping: String, + + /// MOTD when server is starting. + pub motd_starting: String, + + /// Login message when server is starting. + pub login_starting: String, +} diff --git a/src/main.rs b/src/main.rs index d35d27c..4ebebfe 100644 --- a/src/main.rs +++ b/src/main.rs @@ -8,6 +8,7 @@ pub(crate) mod server; pub(crate) mod types; use std::error::Error; +use std::net::SocketAddr; use std::sync::Arc; use bytes::BytesMut; @@ -30,25 +31,38 @@ use server::ServerState; #[tokio::main] async fn main() -> Result<(), ()> { // Initialize logging + // TODO: set default levels! let _ = dotenv::dotenv(); pretty_env_logger::init(); + // Load config + let config = match Config::load() { + Ok(config) => Arc::new(config), + Err(err) => { + error!("Failed to load configuration:"); + error!("{}", err); + return Err(()); + } + }; + let server_state = Arc::new(ServerState::default()); // Listen for new connections // TODO: do not drop error here - let listener = TcpListener::bind(ADDRESS_PUBLIC).await.map_err(|err| { - error!("Failed to start: {}", err); - () - })?; + let listener = TcpListener::bind(config.public.address) + .await + .map_err(|err| { + error!("Failed to start: {}", err); + () + })?; info!( "Proxying egress {} to ingress {}", - ADDRESS_PUBLIC, ADDRESS_PROXY, + config.public.address, config.server.address, ); // Spawn server monitor and signal handler - tokio::spawn(server_monitor(server_state.clone())); + tokio::spawn(server_monitor(config.clone(), server_state.clone())); tokio::spawn(signal_handler(server_state.clone())); // Proxy all incomming connections @@ -57,18 +71,19 @@ async fn main() -> Result<(), ()> { if !server_state.online() { // When server is not online, spawn a status server - let transfer = serve_status(client, inbound, server_state.clone()).map(|r| { - if let Err(err) = r { - error!("Failed to serve status: {:?}", err); - } - }); + let transfer = + serve_status(client, inbound, config.clone(), server_state.clone()).map(|r| { + if let Err(err) = r { + warn!("Failed to serve status: {:?}", err); + } + }); tokio::spawn(transfer); } else { // When server is online, proxy all - let transfer = proxy(inbound, ADDRESS_PROXY.to_string()).map(|r| { + let transfer = proxy(inbound, config.server.address).map(|r| { if let Err(err) = r { - error!("Failed to proxy: {}", err); + warn!("Failed to proxy: {}", err); } }); @@ -91,9 +106,8 @@ pub async fn signal_handler(server_state: Arc) { } /// Server monitor task. -pub async fn server_monitor(state: Arc) { - let addr = ADDRESS_PROXY.parse().expect("invalid server IP"); - monitor::monitor_server(addr, state).await +pub async fn server_monitor(config: Arc, state: Arc) { + monitor::monitor_server(config, state).await } /// Proxy the given inbound stream to a target address. @@ -101,6 +115,7 @@ pub async fn server_monitor(state: Arc) { async fn serve_status( client: Client, mut inbound: TcpStream, + config: Arc, server: Arc, ) -> Result<(), ()> { let (mut reader, mut writer) = inbound.split(); @@ -122,7 +137,7 @@ async fn serve_status( // Hijack login start if client.state() == ClientState::Login && packet.id == proto::LOGIN_PACKET_ID_LOGIN_START { let packet = LoginDisconnect { - reason: Message::new(Payload::text(LABEL_SERVER_STARTING_MESSAGE)), + reason: Message::new(Payload::text(&config.messages.login_starting)), }; let mut data = Vec::new(); @@ -137,7 +152,7 @@ async fn serve_status( if !server.starting() { server.set_starting(true); server.update_last_active_time(); - tokio::spawn(server::start(server).map(|_| ())); + tokio::spawn(server::start(config, server).map(|_| ())); } break; @@ -173,9 +188,9 @@ async fn serve_status( // Select description let description = if server.starting() { - LABEL_SERVER_STARTING + &config.messages.motd_starting } else { - LABEL_SERVER_SLEEPING + &config.messages.motd_sleeping }; // Build status resposne @@ -221,7 +236,7 @@ async fn serve_status( } /// Proxy the inbound stream to a target address. -async fn proxy(mut inbound: TcpStream, addr_target: String) -> Result<(), Box> { +async fn proxy(mut inbound: TcpStream, addr_target: SocketAddr) -> Result<(), Box> { // Set up connection to server // TODO: on connect fail, ping server and redirect to serve_status if offline let mut outbound = TcpStream::connect(addr_target).await?; diff --git a/src/monitor.rs b/src/monitor.rs index 355be40..d0fa1a6 100644 --- a/src/monitor.rs +++ b/src/monitor.rs @@ -13,6 +13,7 @@ use minecraft_protocol::version::v1_14_4::status::StatusResponse; use tokio::io::AsyncWriteExt; use tokio::net::TcpStream; +use crate::config::Config; use crate::proto::{self, ClientState, RawPacket, PROTO_DEFAULT_PROTOCOL}; use crate::server::ServerState; @@ -23,7 +24,10 @@ const MONITOR_PING_INTERVAL: u64 = 2; const STATUS_TIMEOUT: u64 = 8; /// Monitor server. -pub async fn monitor_server(addr: SocketAddr, state: Arc) { +pub async fn monitor_server(config: Arc, state: Arc) { + // Server address + let addr = config.server.address; + loop { // Poll server state and update internal status trace!("Fetching status for {} ... ", addr); @@ -31,8 +35,8 @@ pub async fn monitor_server(addr: SocketAddr, state: Arc) { state.update_status(status); // Sleep server when it's bedtime - if state.should_sleep() { - info!("Server has been idle, initiating sleep..."); + if state.should_sleep(&config) { + info!("Server has been idle, sleeping..."); state.kill_server(); } diff --git a/src/server.rs b/src/server.rs index e59af7e..aca433c 100644 --- a/src/server.rs +++ b/src/server.rs @@ -5,7 +5,7 @@ use std::time::{Duration, Instant}; use minecraft_protocol::data::server_status::ServerStatus; use tokio::process::Command; -use crate::config::{SERVER_CMD, SLEEP_DELAY}; +use crate::config::Config; /// Shared server state. #[derive(Default, Debug)] @@ -127,7 +127,7 @@ impl ServerState { } /// Check whether the server should now sleep. - pub fn should_sleep(&self) -> bool { + pub fn should_sleep(&self, config: &Config) -> bool { // TODO: when initating server start, set last active time! // TODO: do not initiate sleep when starting? // TODO: do not initiate sleep when already initiated (with timeout) @@ -139,7 +139,7 @@ impl ServerState { // Last active time must have passed sleep threshold if let Some(last_idle) = self.last_active.lock().unwrap().as_ref() { - return last_idle.elapsed() >= Duration::from_secs(SLEEP_DELAY); + return last_idle.elapsed() >= Duration::from_secs(config.time.sleep_after as u64); } false @@ -147,8 +147,22 @@ impl ServerState { } /// Start Minecraft server. -pub async fn start(state: Arc) -> Result<(), Box> { - let mut cmd = Command::new(SERVER_CMD); +pub async fn start( + config: Arc, + state: Arc, +) -> Result<(), Box> { + // TODO: this doesn't properly handle quotes + let args = config + .server + .command + .split_terminator(" ") + .collect::>(); + + // Build command + let mut cmd = Command::new(args[0]); + cmd.args(args.iter().skip(1)); + cmd.current_dir(&config.server.directory); + cmd.kill_on_drop(true); info!("Starting server..."); let mut child = cmd.spawn()?;