diff --git a/Cargo.lock b/Cargo.lock index 9ae72b0..220997b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -23,6 +23,123 @@ version = "1.0.45" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ee10e43ae4a853c0a3591d4e2ada1719e553be18199d9da9d4a83f5927c2f5c7" +[[package]] +name = "async-channel" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2114d64672151c0c5eaa5e131ec84a74f06e1e559830dabba01ca30605d66319" +dependencies = [ + "concurrent-queue", + "event-listener", + "futures-core", +] + +[[package]] +name = "async-executor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "871f9bb5e0a22eeb7e8cf16641feb87c9dc67032ccf8ff49e772eb9941d3a965" +dependencies = [ + "async-task", + "concurrent-queue", + "fastrand", + "futures-lite", + "once_cell", + "slab", +] + +[[package]] +name = "async-global-executor" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9586ec52317f36de58453159d48351bc244bc24ced3effc1fce22f3d48664af6" +dependencies = [ + "async-channel", + "async-executor", + "async-io", + "async-mutex", + "blocking", + "futures-lite", + "num_cpus", + "once_cell", +] + +[[package]] +name = "async-io" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a811e6a479f2439f0c04038796b5cfb3d2ad56c230e0f2d3f7b04d68cfee607b" +dependencies = [ + "concurrent-queue", + "futures-lite", + "libc", + "log", + "once_cell", + "parking", + "polling", + "slab", + "socket2", + "waker-fn", + "winapi", +] + +[[package]] +name = "async-lock" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6a8ea61bf9947a1007c5cada31e647dbc77b103c679858150003ba697ea798b" +dependencies = [ + "event-listener", +] + +[[package]] +name = "async-mutex" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479db852db25d9dbf6204e6cb6253698f175c15726470f78af0d918e99d6156e" +dependencies = [ + "event-listener", +] + +[[package]] +name = "async-std" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8056f1455169ab86dd47b47391e4ab0cbd25410a70e9fe675544f49bafaf952" +dependencies = [ + "async-channel", + "async-global-executor", + "async-io", + "async-lock", + "crossbeam-utils", + "futures-channel", + "futures-core", + "futures-io", + "futures-lite", + "gloo-timers", + "kv-log-macro", + "log", + "memchr", + "num_cpus", + "once_cell", + "pin-project-lite", + "pin-utils", + "slab", + "wasm-bindgen-futures", +] + +[[package]] +name = "async-task" +version = "4.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91831deabf0d6d7ec49552e489aed63b7456a7a3c46cff62adad428110b0af0" + +[[package]] +name = "atomic-waker" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "065374052e7df7ee4047b1160cca5e1467a12351a40b3da123c870ba0b8eda2a" + [[package]] name = "atty" version = "0.2.14" @@ -52,6 +169,26 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "blocking" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5e170dbede1f740736619b776d7251cb1b9095c435c34d8ca9f57fcd2f335e9" +dependencies = [ + "async-channel", + "async-task", + "atomic-waker", + "fastrand", + "futures-lite", + "once_cell", +] + +[[package]] +name = "bumpalo" +version = "3.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f1e260c3a9040a7c19a12468758f4c16f31a81a1fe087482be9570ec864bb6c" + [[package]] name = "byteorder" version = "1.4.3" @@ -64,6 +201,18 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8" +[[package]] +name = "cache-padded" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "631ae5198c9be5e753e5cc215e1bd73c2b466a3565173db433f52bb9d3e66dba" + +[[package]] +name = "cc" +version = "1.0.71" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79c2681d6594606957bbb8631c4b90a7fcaaa72cdb714743a437b156d6a7eedd" + [[package]] name = "cfg-if" version = "1.0.0" @@ -121,6 +270,15 @@ dependencies = [ "winapi", ] +[[package]] +name = "concurrent-queue" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30ed07550be01594c6026cff2a1d7fe9c8f683caa798e12b68694ac9e88286a3" +dependencies = [ + "cache-padded", +] + [[package]] name = "crc32fast" version = "1.2.1" @@ -130,6 +288,26 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "crossbeam-utils" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d82cfc11ce7f2c3faef78d8a684447b40d503d9681acebed6cb728d45940c4db" +dependencies = [ + "cfg-if", + "lazy_static", +] + +[[package]] +name = "ctor" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccc0a48a9b826acdf4028595adc9db92caea352f7af011a3034acd172a52a0aa" +dependencies = [ + "quote", + "syn", +] + [[package]] name = "darling" version = "0.12.4" @@ -215,6 +393,35 @@ dependencies = [ "termcolor", ] +[[package]] +name = "err-derive" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcc7f65832b62ed38939f98966824eb6294911c3629b0e9a262bfb80836d9686" +dependencies = [ + "proc-macro-error", + "proc-macro2", + "quote", + "rustversion", + "syn", + "synstructure", +] + +[[package]] +name = "event-listener" +version = "2.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7531096570974c3a9dcf9e4b8e1cede1ec26cf5046219fb3b9d897503b9be59" + +[[package]] +name = "fastrand" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b394ed3d285a429378d3b384b9eb1285267e7df4b166df24b7a6939a04dc392e" +dependencies = [ + "instant", +] + [[package]] name = "flate2" version = "1.0.22" @@ -275,6 +482,21 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "522de2a0fe3e380f1bc577ba0474108faf3f6b18321dbf60b3b9c39a75073377" +[[package]] +name = "futures-lite" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7694489acd39452c77daa48516b894c153f192c3578d5a839b62c58099fcbf48" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "memchr", + "parking", + "pin-project-lite", + "waker-fn", +] + [[package]] name = "futures-sink" version = "0.3.17" @@ -301,6 +523,30 @@ dependencies = [ "pin-utils", ] +[[package]] +name = "getrandom" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fcd999463524c52659517fe2cea98493cfe485d10565e7b0fb07dbba7ad2753" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "gloo-timers" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47204a46aaff920a1ea58b11d03dec6f704287d27561724a4631e450654a891f" +dependencies = [ + "futures-channel", + "futures-core", + "js-sys", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "hashbrown" version = "0.11.2" @@ -350,12 +596,39 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "instant" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" +dependencies = [ + "cfg-if", +] + [[package]] name = "itoa" version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4" +[[package]] +name = "js-sys" +version = "0.3.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cc9ffccd38c451a86bf13657df244e9c3f37493cce8e5e21e940963777acc84" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "kv-log-macro" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de8b303297635ad57c9f5059fd9cee7a47f8e8daa09df0fcd07dd39fb22977f" +dependencies = [ + "log", +] + [[package]] name = "lazy_static" version = "1.4.0" @@ -377,6 +650,8 @@ dependencies = [ "log", "minecraft-protocol", "pretty_env_logger", + "rand 0.8.4", + "rcon", "serde", "thiserror", "tokio", @@ -402,6 +677,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710" dependencies = [ "cfg-if", + "value-bag", ] [[package]] @@ -510,6 +786,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "parking" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "427c3892f9e783d91cc128285287e70a59e206ca452770ece88a76f7a3eddd72" + [[package]] name = "pin-project-lite" version = "0.2.7" @@ -522,6 +804,25 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "polling" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92341d779fa34ea8437ef4d82d440d5e1ce3f3ff7f824aa64424cd481f9a1f25" +dependencies = [ + "cfg-if", + "libc", + "log", + "wepoll-ffi", + "winapi", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed0cfbc8191465bed66e1718596ee0b0b35d5ee1f41c5df2189d0fe8bde535ba" + [[package]] name = "pretty_env_logger" version = "0.4.0" @@ -588,9 +889,9 @@ checksum = "6d71dacdc3c88c1fde3885a3be3fbab9f35724e6ce99467f7d9c5026132184ca" dependencies = [ "autocfg 0.1.7", "libc", - "rand_chacha", + "rand_chacha 0.1.1", "rand_core 0.4.2", - "rand_hc", + "rand_hc 0.1.0", "rand_isaac", "rand_jitter", "rand_os", @@ -599,6 +900,18 @@ dependencies = [ "winapi", ] +[[package]] +name = "rand" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e7573632e6454cf6b99d7aac4ccca54be06da05aca2ef7423d22d27d4d4bcd8" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.3", + "rand_hc 0.3.1", +] + [[package]] name = "rand_chacha" version = "0.1.1" @@ -609,6 +922,16 @@ dependencies = [ "rand_core 0.3.1", ] +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.3", +] + [[package]] name = "rand_core" version = "0.3.1" @@ -624,6 +947,15 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc" +[[package]] +name = "rand_core" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7" +dependencies = [ + "getrandom", +] + [[package]] name = "rand_hc" version = "0.1.0" @@ -633,6 +965,15 @@ dependencies = [ "rand_core 0.3.1", ] +[[package]] +name = "rand_hc" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d51e9f596de227fda2ea6c84607f5558e196eeaf43c986b724ba4fb8fdf497e7" +dependencies = [ + "rand_core 0.6.3", +] + [[package]] name = "rand_isaac" version = "0.1.1" @@ -686,6 +1027,17 @@ dependencies = [ "rand_core 0.3.1", ] +[[package]] +name = "rcon" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "465a6f903164a399084787547a026b83e7937bc576d8acdbd9e41ebf5de90a85" +dependencies = [ + "async-std", + "bytes", + "err-derive", +] + [[package]] name = "rdrand" version = "0.4.0" @@ -712,6 +1064,12 @@ version = "0.6.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" +[[package]] +name = "rustversion" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61b3909d758bb75c79f23d4736fac9433868679d3ad2ea7a61e3c25cfda9a088" + [[package]] name = "ryu" version = "1.0.5" @@ -758,6 +1116,22 @@ dependencies = [ "libc", ] +[[package]] +name = "slab" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9def91fd1e018fe007022791f865d0ccc9b3a0d5001e01aabb8b40e46000afb5" + +[[package]] +name = "socket2" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dc90fe6c7be1a323296982db1836d1ea9e47b6839496dde9a541bc496df3516" +dependencies = [ + "libc", + "winapi", +] + [[package]] name = "strsim" version = "0.10.0" @@ -775,6 +1149,18 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "synstructure" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f36bdaa60a83aca3921b5259d5400cbf5e90fc51931376a9bd4a0eb79aa7210f" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "unicode-xid", +] + [[package]] name = "termcolor" version = "1.1.2" @@ -885,16 +1271,123 @@ version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90dbc611eb48397705a6b0f6e917da23ae517e4d127123d2cf7674206627d32a" dependencies = [ - "rand", + "rand 0.6.5", "serde", ] +[[package]] +name = "value-bag" +version = "1.0.0-alpha.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79923f7731dc61ebfba3633098bf3ac533bbd35ccd8c57e7088d9a5eebe0263f" +dependencies = [ + "ctor", + "version_check", +] + [[package]] name = "version_check" version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe" +[[package]] +name = "waker-fn" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d5b2c62b4012a3e1eca5a7e077d13b3bf498c4073e33ccd58626607748ceeca" + +[[package]] +name = "wasi" +version = "0.10.2+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" + +[[package]] +name = "wasm-bindgen" +version = "0.2.78" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "632f73e236b219150ea279196e54e610f5dbafa5d61786303d4da54f84e47fce" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.78" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a317bf8f9fba2476b4b2c85ef4c4af8ff39c3c7f0cdfeed4f82c34a880aa837b" +dependencies = [ + "bumpalo", + "lazy_static", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e8d7523cb1f2a4c96c1317ca690031b714a51cc14e05f712446691f413f5d39" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.78" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d56146e7c495528bf6587663bea13a8eb588d39b36b679d83972e1a2dbbdacf9" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.78" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7803e0eea25835f8abdc585cd3021b3deb11543c6fe226dcd30b228857c5c5ab" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.78" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0237232789cf037d5480773fe568aac745bfe2afbc11a863e97901780a6b47cc" + +[[package]] +name = "web-sys" +version = "0.3.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38eb105f1c59d9eaa6b5cdc92b859d85b926e82cb2e0945cd0c9259faa6fe9fb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wepoll-ffi" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d743fdedc5c64377b5fc2bc036b01c7fd642205a0d96356034ae3404d49eb7fb" +dependencies = [ + "cc", +] + [[package]] name = "winapi" version = "0.3.9" diff --git a/Cargo.toml b/Cargo.toml index 8ab4548..65fc044 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,6 +18,10 @@ exclude = [ ] edition = "2021" +[features] +default = ["rcon"] +rcon = ["rust_rcon", "rand"] + [dependencies] anyhow = "1.0" bytes = "1.1" @@ -34,3 +38,7 @@ serde = "1.0" thiserror = "1.0" tokio = { version = "1", default-features = false, features = ["rt", "rt-multi-thread", "io-util", "net", "macros", "time", "process", "signal"] } toml = "0.5" + +# Feature: rcon +rust_rcon = { package = "rcon", version = "0.5", optional = true } +rand = { version = "0.8", optional = true } diff --git a/README.md b/README.md index 27acc6d..863eede 100644 --- a/README.md +++ b/README.md @@ -28,8 +28,9 @@ https://user-images.githubusercontent.com/856222/140804726-ba1a8e59-85d9-413b-82 ## Requirements -- Linux, macOS (Windows not yet supported) +- Linux, macOS, Windows - Minecraft Java Edition 1.6 or above +- On Windows: RCON (automatically managed by default) Using a modded Minecraft server and client (such as Forge) should work fine. diff --git a/build.rs b/build.rs new file mode 100644 index 0000000..9c41947 --- /dev/null +++ b/build.rs @@ -0,0 +1,7 @@ +fn main() { + // Must enable rcon on Windows + #[cfg(all(windows, not(feature = "rcon")))] + { + println!("cargo:warning=lazymc: you must enable rcon feature on Windows"); + } +} diff --git a/res/lazymc.toml b/res/lazymc.toml index 7cad408..64aa2b6 100644 --- a/res/lazymc.toml +++ b/res/lazymc.toml @@ -41,6 +41,20 @@ motd_starting = "§2☻ Server is starting...\n§7⌛ Please wait..." # Login (kick) message when server is starting. login_starting = "Server is starting... §c♥§r\n\nThis may take some time.\n\nPlease try to reconnect in a minute." +[rcon] +# Enable sleeping server through RCON. +# Must be enabled on Windows. +enabled = true + +# Server RCON port. Must differ from Minecraft server port. +port = 25575 + +# Server RCON password. +password = "" + +# Randomize ingress server RCON password on each start. +randomize_password = true + [advanced] -# Automatically set IP and port in Minecraft server.properties file. +# Automatically update values in Minecraft server.properties file as required. rewrite_server_properties = true diff --git a/src/action/start.rs b/src/action/start.rs index 450fd3f..bc8bb5f 100644 --- a/src/action/start.rs +++ b/src/action/start.rs @@ -7,19 +7,111 @@ use crate::config::{self, Config}; use crate::mc::server_properties; use crate::service; +/// RCON randomized password length. +#[cfg(feature = "rcon")] +const RCON_PASSWORD_LENGTH: usize = 32; + /// Start lazymc. pub async fn invoke(matches: &ArgMatches) -> Result<(), ()> { // Load config - let config = Arc::new(config::load(matches)); + #[allow(unused_mut)] + let mut config = config::load(matches); + + // Prepare RCON if enabled + #[cfg(feature = "rcon")] + prepare_rcon(&mut config); // Rewrite server server.properties file rewrite_server_properties(&config); // Start server service // TODO: start tokio runtime here? + let config = Arc::new(config); service::server::service(config).await } +/// Prepare RCON. +#[cfg(feature = "rcon")] +fn prepare_rcon(config: &mut Config) { + use crate::util::error::{quit_error_msg, ErrorHintsBuilder}; + + // On Windows, this must be enabled + if cfg!(windows) && !config.rcon.enabled { + quit_error_msg( + "RCON must be enabled on Windows", + ErrorHintsBuilder::default() + .add_info("change 'rcon.enabled' to 'true' in the config file".into()) + .build() + .unwrap(), + ); + } + + // Skip if not enabled + if !config.rcon.enabled { + return; + } + + // Must configure RCON password with no randomization + if config.server.address.port() == config.rcon.port { + quit_error_msg( + "RCON port cannot be the same as the server", + ErrorHintsBuilder::default() + .add_info("change 'rcon.port' in the config file".into()) + .build() + .unwrap(), + ); + } + + // Must configure RCON password with no randomization + if config.rcon.password.trim().is_empty() && !config.rcon.randomize_password { + quit_error_msg( + "RCON password can't be empty, or enable randomization", + ErrorHintsBuilder::default() + .add_info("change 'rcon.randomize_password' to 'true' in the config file".into()) + .add_info("or change 'rcon.password' in the config file".into()) + .build() + .unwrap(), + ); + } + + // RCON password randomization + if config.rcon.randomize_password { + // Must enable server.properties rewrite + if !config.advanced.rewrite_server_properties { + quit_error_msg( + format!( + "You must enable {} rewrite to use RCON password randomization", + server_properties::FILE + ), + ErrorHintsBuilder::default() + .add_info( + "change 'advanced.rewrite_server_properties' to 'true' in the config file" + .into(), + ) + .build() + .unwrap(), + ); + } + + // Randomize password + config.rcon.password = generate_random_password(); + } +} + +/// Generate secure random password. +#[cfg(feature = "rcon")] +fn generate_random_password() -> String { + use rand::{distributions::Alphanumeric, Rng}; + use std::iter; + + let mut rng = rand::thread_rng(); + iter::repeat(()) + .map(|()| rng.sample(Alphanumeric)) + .map(char::from) + .take(RCON_PASSWORD_LENGTH) + .collect() +} + /// Rewrite server server.properties file with correct internal IP and port. fn rewrite_server_properties(config: &Config) { // Rewrite must be enabled @@ -37,12 +129,23 @@ fn rewrite_server_properties(config: &Config) { }; // Build list of changes - let changes = HashMap::from([ + #[allow(unused_mut)] + let mut changes = HashMap::from([ ("server-ip", config.server.address.ip().to_string()), ("server-port", config.server.address.port().to_string()), ("query.port", config.server.address.port().to_string()), ]); + // Add RCON configuration + #[cfg(feature = "rcon")] + if config.rcon.enabled { + changes.extend([ + ("rcon.port", config.rcon.port.to_string()), + ("rcon.password", config.rcon.password.clone()), + ("enable-rcon", "true".into()), + ]); + } + // Rewrite file server_properties::rewrite_dir(dir, changes) } diff --git a/src/config.rs b/src/config.rs index 624e9e7..9db086f 100644 --- a/src/config.rs +++ b/src/config.rs @@ -69,6 +69,9 @@ pub struct Config { /// Messages, shown to the user. pub messages: Messages, + /// RCON configuration. + pub rcon: Rcon, + /// Advanced configuration. pub advanced: Advanced, } @@ -132,6 +135,22 @@ pub struct Messages { pub login_starting: String, } +/// RCON configuration. +#[derive(Debug, Deserialize)] +pub struct Rcon { + /// Enable sleeping server through RCON. + pub enabled: bool, + + /// Server RCON port. + pub port: u16, + + /// Server RCON password. + pub password: String, + + /// Randomize ingress server RCON password on each start. + pub randomize_password: bool, +} + /// Advanced configuration. #[derive(Debug, Deserialize)] pub struct Advanced { diff --git a/src/mc/mod.rs b/src/mc/mod.rs index 22923fe..a54afeb 100644 --- a/src/mc/mod.rs +++ b/src/mc/mod.rs @@ -1 +1,3 @@ +#[cfg(feature = "rcon")] +pub mod rcon; pub mod server_properties; diff --git a/src/mc/rcon.rs b/src/mc/rcon.rs new file mode 100644 index 0000000..699b1d3 --- /dev/null +++ b/src/mc/rcon.rs @@ -0,0 +1,29 @@ +use rust_rcon::{Connection, Error as RconError}; + +/// An RCON client. +pub struct Rcon { + con: Connection, +} + +impl Rcon { + /// Connect to a host. + pub async fn connect(addr: &str, pass: &str) -> Result { + // Start connection + let con = Connection::builder() + .enable_minecraft_quirks(true) + .connect(addr, pass) + .await + .map_err(|err| { + dbg!(err); + () + })?; + + Ok(Self { con }) + } + + /// Send command over RCON. + pub async fn cmd(&mut self, cmd: &str) -> Result { + debug!(target: "lazymc::rcon", "Sending RCON: {}", cmd); + self.con.cmd(cmd).await + } +} diff --git a/src/mc/server_properties.rs b/src/mc/server_properties.rs index 7556ee9..0b14651 100644 --- a/src/mc/server_properties.rs +++ b/src/mc/server_properties.rs @@ -77,7 +77,7 @@ pub fn rewrite_file(file: &Path, changes: HashMap<&str, String>) { match fs::write(file, contents) { Ok(_) => { info!(target: "lazymc", - "Rewritten {} file with correct IP and port", + "Rewritten {} file with updated values", FILE, ); } diff --git a/src/monitor.rs b/src/monitor.rs index c3c5cbd..69bc083 100644 --- a/src/monitor.rs +++ b/src/monitor.rs @@ -37,7 +37,9 @@ pub async fn monitor_server(config: Arc, state: Arc) { // Sleep server when it's bedtime if state.should_sleep(&config) { info!(target: "lazymc::montior", "Server has been idle, sleeping..."); - state.kill_server(); + if !state.kill_server(&config).await { + warn!(target: "lazymc", "Failed to stop server"); + } } // TODO: use interval instead, for a more reliable polling interval? diff --git a/src/server.rs b/src/server.rs index de113f7..84ff26a 100644 --- a/src/server.rs +++ b/src/server.rs @@ -61,20 +61,31 @@ impl ServerState { } /// Kill any running server. - pub fn kill_server(&self) -> bool { - if let Some(pid) = *self.pid.lock().unwrap() { - debug!(target: "lazymc", "Sending kill signal to server"); - crate::os::unix::kill_gracefully(pid); + #[allow(unused_variables)] + pub async fn kill_server(&self, config: &Config) -> bool { + // Ensure we have a running process + let has_process = self.pid.lock().unwrap().is_some(); + if !has_process { + return false; + } - // TODO: should we set this? - self.set_online(false); - self.set_keep_online_until(None); + // Try to kill through RCON + #[cfg(feature = "rcon")] + if stop_server_rcon(config, &self).await { + // TODO: set stopping state elsewhere + self.stopping.store(true, Ordering::Relaxed); return true; } - // TODO: set stopping state elsewhere - self.stopping.store(true, Ordering::Relaxed); + // Try to kill through signal + #[cfg(unix)] + if stop_server_signal(&self) { + // TODO: set stopping state elsewhere + self.stopping.store(true, Ordering::Relaxed); + + return true; + } false } @@ -240,22 +251,62 @@ pub async fn invoke_server_command( Ok(()) } -/// Gracefully kill process. -fn kill_gracefully(pid: u32) { - #[cfg(unix)] - unsafe { - debug!(target: "lazymc", "Sending SIGTERM signal to {} to kill server", pid); - let result = libc::kill(pid as i32, libc::SIGTERM); - trace!(target: "lazymc", "SIGTERM result: {}", result); +/// Stop server through RCON. +#[cfg(feature = "rcon")] +async fn stop_server_rcon(config: &Config, server: &ServerState) -> bool { + use crate::mc::rcon::Rcon; - // TODO: send sigterm to childs as well? - // TODO: handle error if != 0 + // RCON must be enabled + if !config.rcon.enabled { + return false; } - // TODO: implement for Windows - #[cfg(not(unix))] - { - // TODO: implement this for Windows - unimplemented!(); + // RCON address + let mut addr = config.server.address.clone(); + addr.set_port(config.rcon.port); + let addr = addr.to_string(); + + // Create RCON client + let mut rcon = match Rcon::connect(&addr, &config.rcon.password).await { + Ok(rcon) => rcon, + Err(_) => { + error!(target: "lazymc", "failed to create RCON client to sleep server"); + return false; + } + }; + + // Invoke save-all + if let Err(err) = rcon.cmd("save-all").await { + error!(target: "lazymc", "failed to invoke save-all through RCON, ignoring: {}", err); } + + // Invoke stop + if let Err(err) = rcon.cmd("stop").await { + error!(target: "lazymc", "failed to invoke stop through RCON: {}", err); + } + + // TODO: should we set this? + server.set_online(false); + server.set_keep_online_until(None); + + true +} + +/// Stop server by sending SIGTERM signal. +/// +/// Only works on Unix. +#[cfg(unix)] +fn stop_server_signal(server: &ServerState) -> bool { + if let Some(pid) = *server.pid.lock().unwrap() { + debug!(target: "lazymc", "Sending kill signal to server"); + crate::os::kill_gracefully(pid); + + // TODO: should we set this? + server.set_online(false); + server.set_keep_online_until(None); + + return true; + } + + false } diff --git a/src/service/server.rs b/src/service/server.rs index 62002b2..0b331f8 100644 --- a/src/service/server.rs +++ b/src/service/server.rs @@ -39,7 +39,10 @@ pub async fn service(config: Arc) -> Result<(), ()> { config.clone(), server_state.clone(), )); - tokio::spawn(service::signal::service(server_state.clone())); + tokio::spawn(service::signal::service( + config.clone(), + server_state.clone(), + )); // Initiate server start if config.server.wake_on_start { diff --git a/src/service/signal.rs b/src/service/signal.rs index d20d223..ec0c72e 100644 --- a/src/service/signal.rs +++ b/src/service/signal.rs @@ -1,12 +1,19 @@ use std::sync::Arc; +use crate::config::Config; use crate::server::ServerState; /// Signal handler task. -pub async fn service(server_state: Arc) { +pub async fn service(config: Arc, server_state: Arc) { loop { + // Wait for SIGTERM/SIGINT signal tokio::signal::ctrl_c().await.unwrap(); - if !server_state.kill_server() { + + // Attemp to kill server + let killed = !server_state.kill_server(&config).await; + + // If we don't kill the server, quit this process + if !killed { // TODO: gracefully kill itself instead std::process::exit(1) }