Add configuration

This commit is contained in:
timvisee 2021-11-08 15:26:36 +01:00
parent c0b3677cb0
commit 0c3685c37c
No known key found for this signature in database
GPG Key ID: B8DB720BC383E172
7 changed files with 152 additions and 44 deletions

1
.gitignore vendored
View File

@ -1,4 +1,5 @@
.env .env
lazymc.toml
/target /target
# Test server # Test server

11
Cargo.lock generated
View File

@ -216,7 +216,9 @@ dependencies = [
"log", "log",
"minecraft-protocol", "minecraft-protocol",
"pretty_env_logger", "pretty_env_logger",
"serde",
"tokio", "tokio",
"toml",
] ]
[[package]] [[package]]
@ -611,6 +613,15 @@ dependencies = [
"syn", "syn",
] ]
[[package]]
name = "toml"
version = "0.5.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a31142970826733df8241ef35dc040ef98c679ab14d7c3e54d827099b3acecaa"
dependencies = [
"serde",
]
[[package]] [[package]]
name = "unicode-xid" name = "unicode-xid"
version = "0.2.2" version = "0.2.2"

View File

@ -28,3 +28,6 @@ tokio = { version = "1", default-features = false, features = ["rt", "rt-multi-t
dotenv = "0.15" dotenv = "0.15"
log = "0.4" log = "0.4"
pretty_env_logger = "0.4" pretty_env_logger = "0.4"
serde = "1.0"
toml = "0.5"

View File

@ -1,21 +1,81 @@
/// Command to start server. use std::fs;
pub const SERVER_CMD: &str = "/home/timvisee/git/lazymc/mcserver/start"; use std::io;
use std::net::SocketAddr;
use std::path::PathBuf;
/// Public address for users to connect to. use serde::Deserialize;
pub const ADDRESS_PUBLIC: &str = "127.0.0.1:9090";
/// Minecraft server address to proxy to. /// Default configuration file location.
pub const ADDRESS_PROXY: &str = "127.0.0.1:9091"; const CONFIG_FILE: &str = "lazymc.toml";
/// Server description shown when server is starting. /// Configuration.
pub const LABEL_SERVER_SLEEPING: &str = "☠ Server is sleeping\n§2☻ Join to start it up"; #[derive(Debug, Deserialize)]
pub struct Config {
/// Public configuration.
pub public: Public,
/// Server description shown when server is starting. /// Server configuration.
pub const LABEL_SERVER_STARTING: &str = "§2☻ Server is starting...\n§7⌛ Please wait..."; pub server: Server,
/// Kick message shown when user tries to connect to starting server. /// Time configuration.
pub const LABEL_SERVER_STARTING_MESSAGE: &str = pub time: Time,
"Server is starting... §c♥§r\n\nThis may take some time.\n\nPlease try to reconnect in a minute.";
/// Idle server sleeping delay in seconds. /// Messages, shown to the user.
pub const SLEEP_DELAY: u64 = 10; pub messages: Messages,
}
impl Config {
/// Load configuration form file.
pub fn load() -> Result<Self, io::Error> {
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,
}

View File

@ -8,6 +8,7 @@ pub(crate) mod server;
pub(crate) mod types; pub(crate) mod types;
use std::error::Error; use std::error::Error;
use std::net::SocketAddr;
use std::sync::Arc; use std::sync::Arc;
use bytes::BytesMut; use bytes::BytesMut;
@ -30,25 +31,38 @@ use server::ServerState;
#[tokio::main] #[tokio::main]
async fn main() -> Result<(), ()> { async fn main() -> Result<(), ()> {
// Initialize logging // Initialize logging
// TODO: set default levels!
let _ = dotenv::dotenv(); let _ = dotenv::dotenv();
pretty_env_logger::init(); 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()); let server_state = Arc::new(ServerState::default());
// Listen for new connections // Listen for new connections
// TODO: do not drop error here // TODO: do not drop error here
let listener = TcpListener::bind(ADDRESS_PUBLIC).await.map_err(|err| { let listener = TcpListener::bind(config.public.address)
error!("Failed to start: {}", err); .await
() .map_err(|err| {
})?; error!("Failed to start: {}", err);
()
})?;
info!( info!(
"Proxying egress {} to ingress {}", "Proxying egress {} to ingress {}",
ADDRESS_PUBLIC, ADDRESS_PROXY, config.public.address, config.server.address,
); );
// Spawn server monitor and signal handler // 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())); tokio::spawn(signal_handler(server_state.clone()));
// Proxy all incomming connections // Proxy all incomming connections
@ -57,18 +71,19 @@ async fn main() -> Result<(), ()> {
if !server_state.online() { if !server_state.online() {
// When server is not online, spawn a status server // When server is not online, spawn a status server
let transfer = serve_status(client, inbound, server_state.clone()).map(|r| { let transfer =
if let Err(err) = r { serve_status(client, inbound, config.clone(), server_state.clone()).map(|r| {
error!("Failed to serve status: {:?}", err); if let Err(err) = r {
} warn!("Failed to serve status: {:?}", err);
}); }
});
tokio::spawn(transfer); tokio::spawn(transfer);
} else { } else {
// When server is online, proxy all // 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 { 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<ServerState>) {
} }
/// Server monitor task. /// Server monitor task.
pub async fn server_monitor(state: Arc<ServerState>) { pub async fn server_monitor(config: Arc<Config>, state: Arc<ServerState>) {
let addr = ADDRESS_PROXY.parse().expect("invalid server IP"); monitor::monitor_server(config, state).await
monitor::monitor_server(addr, state).await
} }
/// Proxy the given inbound stream to a target address. /// Proxy the given inbound stream to a target address.
@ -101,6 +115,7 @@ pub async fn server_monitor(state: Arc<ServerState>) {
async fn serve_status( async fn serve_status(
client: Client, client: Client,
mut inbound: TcpStream, mut inbound: TcpStream,
config: Arc<Config>,
server: Arc<ServerState>, server: Arc<ServerState>,
) -> Result<(), ()> { ) -> Result<(), ()> {
let (mut reader, mut writer) = inbound.split(); let (mut reader, mut writer) = inbound.split();
@ -122,7 +137,7 @@ async fn serve_status(
// Hijack login start // Hijack login start
if client.state() == ClientState::Login && packet.id == proto::LOGIN_PACKET_ID_LOGIN_START { if client.state() == ClientState::Login && packet.id == proto::LOGIN_PACKET_ID_LOGIN_START {
let packet = LoginDisconnect { 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(); let mut data = Vec::new();
@ -137,7 +152,7 @@ async fn serve_status(
if !server.starting() { if !server.starting() {
server.set_starting(true); server.set_starting(true);
server.update_last_active_time(); server.update_last_active_time();
tokio::spawn(server::start(server).map(|_| ())); tokio::spawn(server::start(config, server).map(|_| ()));
} }
break; break;
@ -173,9 +188,9 @@ async fn serve_status(
// Select description // Select description
let description = if server.starting() { let description = if server.starting() {
LABEL_SERVER_STARTING &config.messages.motd_starting
} else { } else {
LABEL_SERVER_SLEEPING &config.messages.motd_sleeping
}; };
// Build status resposne // Build status resposne
@ -221,7 +236,7 @@ async fn serve_status(
} }
/// Proxy the inbound stream to a target address. /// Proxy the inbound stream to a target address.
async fn proxy(mut inbound: TcpStream, addr_target: String) -> Result<(), Box<dyn Error>> { async fn proxy(mut inbound: TcpStream, addr_target: SocketAddr) -> Result<(), Box<dyn Error>> {
// Set up connection to server // Set up connection to server
// TODO: on connect fail, ping server and redirect to serve_status if offline // TODO: on connect fail, ping server and redirect to serve_status if offline
let mut outbound = TcpStream::connect(addr_target).await?; let mut outbound = TcpStream::connect(addr_target).await?;

View File

@ -13,6 +13,7 @@ use minecraft_protocol::version::v1_14_4::status::StatusResponse;
use tokio::io::AsyncWriteExt; use tokio::io::AsyncWriteExt;
use tokio::net::TcpStream; use tokio::net::TcpStream;
use crate::config::Config;
use crate::proto::{self, ClientState, RawPacket, PROTO_DEFAULT_PROTOCOL}; use crate::proto::{self, ClientState, RawPacket, PROTO_DEFAULT_PROTOCOL};
use crate::server::ServerState; use crate::server::ServerState;
@ -23,7 +24,10 @@ const MONITOR_PING_INTERVAL: u64 = 2;
const STATUS_TIMEOUT: u64 = 8; const STATUS_TIMEOUT: u64 = 8;
/// Monitor server. /// Monitor server.
pub async fn monitor_server(addr: SocketAddr, state: Arc<ServerState>) { pub async fn monitor_server(config: Arc<Config>, state: Arc<ServerState>) {
// Server address
let addr = config.server.address;
loop { loop {
// Poll server state and update internal status // Poll server state and update internal status
trace!("Fetching status for {} ... ", addr); trace!("Fetching status for {} ... ", addr);
@ -31,8 +35,8 @@ pub async fn monitor_server(addr: SocketAddr, state: Arc<ServerState>) {
state.update_status(status); state.update_status(status);
// Sleep server when it's bedtime // Sleep server when it's bedtime
if state.should_sleep() { if state.should_sleep(&config) {
info!("Server has been idle, initiating sleep..."); info!("Server has been idle, sleeping...");
state.kill_server(); state.kill_server();
} }

View File

@ -5,7 +5,7 @@ use std::time::{Duration, Instant};
use minecraft_protocol::data::server_status::ServerStatus; use minecraft_protocol::data::server_status::ServerStatus;
use tokio::process::Command; use tokio::process::Command;
use crate::config::{SERVER_CMD, SLEEP_DELAY}; use crate::config::Config;
/// Shared server state. /// Shared server state.
#[derive(Default, Debug)] #[derive(Default, Debug)]
@ -127,7 +127,7 @@ impl ServerState {
} }
/// Check whether the server should now sleep. /// 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: when initating server start, set last active time!
// TODO: do not initiate sleep when starting? // TODO: do not initiate sleep when starting?
// TODO: do not initiate sleep when already initiated (with timeout) // TODO: do not initiate sleep when already initiated (with timeout)
@ -139,7 +139,7 @@ impl ServerState {
// Last active time must have passed sleep threshold // Last active time must have passed sleep threshold
if let Some(last_idle) = self.last_active.lock().unwrap().as_ref() { 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 false
@ -147,8 +147,22 @@ impl ServerState {
} }
/// Start Minecraft server. /// Start Minecraft server.
pub async fn start(state: Arc<ServerState>) -> Result<(), Box<dyn std::error::Error>> { pub async fn start(
let mut cmd = Command::new(SERVER_CMD); config: Arc<Config>,
state: Arc<ServerState>,
) -> Result<(), Box<dyn std::error::Error>> {
// TODO: this doesn't properly handle quotes
let args = config
.server
.command
.split_terminator(" ")
.collect::<Vec<_>>();
// 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..."); info!("Starting server...");
let mut child = cmd.spawn()?; let mut child = cmd.spawn()?;