diff --git a/composition.toml b/composition.toml index 36290ef..124f049 100644 --- a/composition.toml +++ b/composition.toml @@ -1,5 +1,6 @@ -favicon = "server-icon.png" +log_level = "debug" max_players = 20 motd = "Hello world!" +ping_game_version = "1.18.1" port = 25565 -log_level = "debug" \ No newline at end of file +server_icon = "server-icon.png" diff --git a/src/config.rs b/src/config.rs index 2d6ec02..6a8cf9a 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,12 +1,23 @@ use crate::prelude::*; +use std::{fs::File, path::Path}; + +fn read_file(path: &Path) -> std::io::Result> { + let mut data = vec![]; + let mut file = File::open(path)?; + file.read_to_end(&mut data)?; + Ok(data) +} pub struct Config { pub port: u16, pub max_players: usize, pub motd: String, - pub favicon: String, + pub server_icon: String, + pub server_icon_bytes: Vec, pub server_string: String, pub log_level: log::LevelFilter, + pub protocol_version: i32, + pub game_version: String, pub server_version: String, } impl Default for Config { @@ -20,33 +31,22 @@ impl Default for Config { port: 25565, max_players: 20, motd: "Hello world!".to_owned(), - favicon: "server-icon.png".to_owned(), + server_icon: "server-icon.png".to_owned(), + server_icon_bytes: include_bytes!("../server-icon.png").to_vec(), server_string: server_version.clone(), log_level: if cfg!(debug_assertions) { log::LevelFilter::Debug } else { log::LevelFilter::Info }, + protocol_version: 756, + game_version: "1.18.1".to_owned(), server_version, } } } impl Config { - pub fn from_file(filename: &str) -> Config { - let read_file = |filename: &str| -> std::io::Result { - use std::{fs::File, io::prelude::*}; - let mut data = String::new(); - let mut file = File::open(filename)?; - file.read_to_string(&mut data)?; - Ok(data) - }; - if let Ok(config) = read_file(filename) { - Config::parse(&config) - } else { - Config::default() - } - } - pub fn parse(data: &str) -> Config { + pub fn from_toml(cfg: toml::Value) -> Config { let mut config = Config::default(); let get_string = |cfg: &toml::Value, field: &str, default: &str, error: &str| -> String { @@ -62,77 +62,185 @@ impl Config { default.to_owned() }; - if let Ok(cfg) = data.parse::() { - if let Some(&toml::Value::Integer(port)) = cfg.get("port") { - if port < u16::MIN as i64 || port > u16::MAX as i64 { - warn!("Config port must be an integer in the range of {}-{}, using default port: {}", u16::MIN, u16::MAX, config.port); - } else { - config.port = port as u16; - } - } else { + if let Some(&toml::Value::Integer(port)) = cfg.get("port") { + if port < u16::MIN as i64 || port > u16::MAX as i64 { warn!( "Config port must be an integer in the range of {}-{}, using default port: {}", u16::MIN, u16::MAX, config.port ); + } else { + config.port = port as u16; } + } else { + warn!( + "Config port must be an integer in the range of {}-{}, using default port: {}", + u16::MIN, + u16::MAX, + config.port + ); + } - if let Some(&toml::Value::Integer(max_players)) = cfg.get("max_players") { - if max_players < 0 { - warn!("Config max_players must be an integer in the range of {}-{}, using default max_players: {}", usize::MIN, usize::MAX, config.max_players); + if let Some(&toml::Value::Integer(max_players)) = cfg.get("max_players") { + if max_players < 0 { + warn!("Config max_players must be an integer in the range of {}-{}, using default max_players: {}", usize::MIN, usize::MAX, config.max_players); + } else { + config.max_players = max_players as usize; + } + } else { + warn!("Config max_players must be an integer in the range of {}-{}, using default max_players: {}", usize::MIN, usize::MAX, config.max_players); + } + + config.motd = get_string( + &cfg, + "motd", + &config.motd, + &format!( + "Config motd must be a string, using default motd: \"{}\"", + config.motd + ), + ); + config.game_version = get_string( + &cfg, + "ping_game_version", + &config.game_version, + &format!( + "Config ping_game_version must be a string, using default ping_game_version: \"{}\"", + config.game_version + ), + ); + config.server_icon = get_string( + &cfg, + "server_icon", + &config.server_icon, + &format!( + "Config server_icon must be a string, using default server_icon: \"{}\"", + config.server_icon + ), + ); + let default_log_level = format!("{}", config.log_level).to_ascii_lowercase(); + config.log_level = match &get_string( + &cfg, + "log_level", + &default_log_level, + &format!( + "Config log_level must be a string, using default log_level: {}", + default_log_level + ), + )[..] + { + "off" => log::LevelFilter::Off, + "error" => log::LevelFilter::Error, + "warn" => log::LevelFilter::Warn, + "info" => log::LevelFilter::Info, + "debug" => log::LevelFilter::Debug, + "trace" => log::LevelFilter::Trace, + _ => { + warn!("Config log_level must be one of the predefined levels: off, error, warn, info, debug, trace"); + config.log_level + } + }; + config + } + pub fn load() -> Config { + let mut config = Config::default(); + + // Load the config + let config_path = Path::new("composition.toml"); + if config_path.exists() { + if let Ok(cfg) = read_file(config_path) { + let cfg = String::from_utf8_lossy(&cfg); + if let Ok(cfg) = cfg.parse::() { + config = Config::from_toml(cfg); } else { - config.max_players = max_players as usize; + error!("Could not parse configuration file"); + std::process::exit(1); } } else { - warn!("Config max_players must be an integer in the range of {}-{}, using default max_players: {}", usize::MIN, usize::MAX, config.max_players); - } - - config.motd = get_string( - &cfg, - "motd", - &config.motd, - &format!( - "Config motd must be a string, using default motd: \"{}\"", - config.motd - ), - ); - config.favicon = get_string( - &cfg, - "favicon", - &config.favicon, - &format!( - "Config favicon must be a string, using default favicon: \"{}\"", - config.favicon - ), - ); - let default_log_level = format!("{}", config.log_level).to_ascii_lowercase(); - config.log_level = match &get_string( - &cfg, - "log_level", - &default_log_level, - &format!( - "Config log_level must be a string, using default log_level: {}", - default_log_level - ), - )[..] - { - "off" => log::LevelFilter::Off, - "error" => log::LevelFilter::Error, - "warn" => log::LevelFilter::Warn, - "info" => log::LevelFilter::Info, - "debug" => log::LevelFilter::Debug, - "trace" => log::LevelFilter::Trace, - _ => { - warn!("Config log_level must be one of the predefined levels: off, error, warn, info, debug, trace"); - config.log_level + warn!( + "Could not read configuration file, creating {}", + config_path.to_str().unwrap_or("") + ); + if config.write(config_path).is_err() { + error!("Could not write configuration file"); + std::process::exit(1); } - }; - - config + } } else { - warn!("Could not parse configuration file, using default"); - config + warn!( + "Configuration file does not exist, creating {}", + config_path.to_str().unwrap_or("") + ); + if config.write(config_path).is_err() { + error!("Could not write configuration file"); + std::process::exit(1); + } } + + // Load the server icon + let server_icon_path = Path::new(&config.server_icon); + if server_icon_path.exists() { + if let Ok(server_icon_bytes) = read_file(server_icon_path) { + config.server_icon_bytes = server_icon_bytes; + } else { + warn!( + "Could not read server icon file, creating {}", + server_icon_path.to_str().unwrap_or("") + ); + if config.write_server_icon(server_icon_path).is_err() { + error!("Could not write server icon file"); + std::process::exit(1); + } + } + } else { + warn!( + "Server icon file does not exist, creating {}", + server_icon_path.to_str().unwrap_or("") + ); + if config.write_server_icon(server_icon_path).is_err() { + error!("Could not write server icon file"); + std::process::exit(1); + } + } + + config + } + pub fn write(&self, path: &Path) -> std::io::Result<()> { + use toml::{map::Map, Value}; + + let config = Value::Table({ + let mut m = Map::new(); + m.insert( + "server_icon".to_owned(), + Value::String(self.server_icon.clone()), + ); + m.insert( + "log_level".to_owned(), + Value::String(format!("{}", self.log_level).to_ascii_lowercase()), + ); + m.insert("max_players".to_owned(), Value::Integer(20)); + m.insert("motd".to_owned(), Value::String(self.motd.clone())); + m.insert( + "ping_game_version".to_owned(), + Value::String(self.game_version.clone()), + ); + m.insert("port".to_owned(), Value::Integer(25565)); + m + }); + if path.exists() { + std::fs::remove_file(path)?; + } + let mut file = File::create(path)?; + file.write_all(&toml::ser::to_vec(&config).unwrap())?; + Ok(()) + } + pub fn write_server_icon(&self, path: &Path) -> std::io::Result<()> { + if path.exists() { + std::fs::remove_file(path)?; + } + let mut file = File::create(path)?; + file.write_all(&self.server_icon_bytes)?; + Ok(()) } } diff --git a/src/lib.rs b/src/lib.rs index c7e571e..bcd2d25 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -8,22 +8,13 @@ pub mod server; use crate::prelude::*; use std::sync::mpsc::{self, Receiver}; -pub static PROTOCOL_VERSION: i32 = 757; lazy_static! { - pub static ref CONFIG: Config = Config::from_file("composition.toml"); - pub static ref FAVICON: std::io::Result> = { - use std::{fs::File, io::prelude::*}; - let mut data = vec![]; - let mut file = File::open(CONFIG.favicon.clone())?; - file.read_to_end(&mut data)?; - Ok(data) - }; + pub static ref CONFIG: Config = Config::load(); pub static ref START_TIME: std::time::Instant = std::time::Instant::now(); } /// Set up logging, read the config file, etc. pub fn init() -> Receiver<()> { - // Load the START_TIME static - lazy_static lazy loads the value when first needed. let _ = START_TIME.elapsed(); // Set up fern logging. fern::Dispatch::new() @@ -57,7 +48,7 @@ pub async fn start_server() -> server::Server { } pub mod prelude { - pub use crate::{config::Config, CONFIG, FAVICON, PROTOCOL_VERSION, START_TIME}; + pub use crate::{config::Config, CONFIG, START_TIME}; pub use log::*; pub use serde::{Deserialize, Serialize}; pub use serde_json::json; @@ -65,8 +56,9 @@ pub mod prelude { pub type JSON = serde_json::Value; pub type NBT = quartz_nbt::NbtCompound; pub use std::collections::VecDeque; + pub use std::io::{Read, Write}; pub use substring::Substring; - pub use tokio::io::{AsyncReadExt, AsyncWriteExt}; + pub use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}; #[derive(Clone, Debug, PartialEq)] pub enum ParseError { NotEnoughData, diff --git a/src/net/packets.rs b/src/net/packets.rs index 90084db..887d681 100644 --- a/src/net/packets.rs +++ b/src/net/packets.rs @@ -222,8 +222,7 @@ impl Packet { "online": current_players, }, "description": description, - // TODO: Add base64 favicon - "favicon": format!("data:image/png;base64,{}", radix64::STD_NO_PAD.encode(FAVICON.as_ref().unwrap())), + "favicon": format!("data:image/png;base64,{}", radix64::STD_NO_PAD.encode(&CONFIG.server_icon_bytes)), })), ), CS01Pong { payload } => (0x01, serialize_long(*payload).to_vec()), diff --git a/src/server/mod.rs b/src/server/mod.rs index e9df164..cd4ed8f 100644 --- a/src/server/mod.rs +++ b/src/server/mod.rs @@ -108,10 +108,12 @@ impl Server { server_port: _, next_state, } => { - if protocol_version != PROTOCOL_VERSION { + if protocol_version != CONFIG.protocol_version + && next_state == NetworkClientState::Login + { debug!( "Disconnecting client {} for mismatched protocols: {} (expected {})", - client.id, protocol_version, PROTOCOL_VERSION + client.id, protocol_version, CONFIG.protocol_version ); client.disconnect(None).await; return Err(()); @@ -121,8 +123,8 @@ impl Server { SS00Request => { let _ = client .send_packet(CS00Response { - version_name: "1.18.1".to_owned(), - protocol_version: PROTOCOL_VERSION, + version_name: CONFIG.game_version.clone(), + protocol_version: CONFIG.protocol_version, max_players: CONFIG.max_players, current_players, description: json!({