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
lazymc.toml
/target
# Test server

11
Cargo.lock generated
View File

@ -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"

View File

@ -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"

View File

@ -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<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;
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| {
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| {
let transfer =
serve_status(client, inbound, config.clone(), server_state.clone()).map(|r| {
if let Err(err) = r {
error!("Failed to serve status: {:?}", err);
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<ServerState>) {
}
/// Server monitor task.
pub async fn server_monitor(state: Arc<ServerState>) {
let addr = ADDRESS_PROXY.parse().expect("invalid server IP");
monitor::monitor_server(addr, state).await
pub async fn server_monitor(config: Arc<Config>, state: Arc<ServerState>) {
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<ServerState>) {
async fn serve_status(
client: Client,
mut inbound: TcpStream,
config: Arc<Config>,
server: Arc<ServerState>,
) -> 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<dyn Error>> {
async fn proxy(mut inbound: TcpStream, addr_target: SocketAddr) -> Result<(), Box<dyn Error>> {
// 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?;

View File

@ -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<ServerState>) {
pub async fn monitor_server(config: Arc<Config>, state: Arc<ServerState>) {
// 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<ServerState>) {
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();
}

View File

@ -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<ServerState>) -> Result<(), Box<dyn std::error::Error>> {
let mut cmd = Command::new(SERVER_CMD);
pub async fn start(
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...");
let mut child = cmd.spawn()?;