20 Commits

Author SHA1 Message Date
timvisee
7da467ff8c Bump version to 0.2.2 2021-11-18 11:37:02 +01:00
timvisee
c9d7af0e3c Add server favicon to status responses 2021-11-18 11:35:49 +01:00
timvisee
0715baed8c Bump version to 0.2.1 2021-11-17 20:31:22 +01:00
timvisee
1f4ec11ad1 Resolve clippy warnings 2021-11-17 20:26:49 +01:00
timvisee
acf6768b49 Show warning if 127.0.0.1 is IP banned 2021-11-17 20:25:49 +01:00
timvisee
75f7b62b16 Kick user with proper ban message, tweak IP file change debounce time 2021-11-17 20:21:22 +01:00
timvisee
9cc1958bbd Automatically reload banned IPs when file changes 2021-11-17 20:15:30 +01:00
timvisee
785bd2f33e Make banned IP format less strict to support possible future changes 2021-11-17 18:46:45 +01:00
timvisee
b168dcefde Respect IP ban expiry times 2021-11-17 18:42:26 +01:00
timvisee
74d772ab42 Respond with real server MOTD if it is currently started 2021-11-17 18:28:55 +01:00
timvisee
a71b3cb24f Add option to drop all connections from banned IPs
This instantly disconnects clients from banned IPs. Clients won't be
able to request or ping the server status. Clients won't get a kick
message with their ban reason either. Clients simply get a
'Disconnected' message on login.
2021-11-17 18:23:17 +01:00
timvisee
28dbcdbfd6 Show MOTD for banned players, kick with reason on login 2021-11-17 18:14:02 +01:00
timvisee
e816d4ff6c Use more efficient structure to manage banned IPs 2021-11-17 17:46:51 +01:00
timvisee
168cbceb4c Disconnect banned IPs based on server banned-ips.json file 2021-11-17 17:36:28 +01:00
timvisee
b1bd9e1837 Add support for host names in config address fields, resolve them to IP 2021-11-17 16:14:05 +01:00
timvisee
ec24f088b2 Fix lobby error due to invalid packet IDs 2021-11-16 18:09:45 +01:00
timvisee
6321999489 Update dependencies 2021-11-16 17:58:09 +01:00
timvisee
47fe7d0387 Extract all packet writing logic to single function 2021-11-16 17:57:34 +01:00
timvisee
7df3829e00 Use u8 for packet IDs 2021-11-16 17:13:30 +01:00
timvisee
b06f26b3e8 Refactor, cleanup status logic, extract join occupy logic into modules 2021-11-16 17:05:44 +01:00
31 changed files with 1653 additions and 778 deletions

View File

@@ -1,5 +1,15 @@
# Changelog
## 0.2.2 (2021-11-18)
- Add server favicon to status response
## 0.2.1 (2021-11-17)
- Add support for using host names in config address fields
- Handle banned players within `lazymc` based on server `banned-ips.json`
- Update dependencies
## 0.2.0 (2021-11-15)
- Add lockout feature, enable to kick all connecting clients with a message

355
Cargo.lock generated
View File

@@ -80,7 +80,7 @@ dependencies = [
"slab",
"socket2",
"waker-fn",
"winapi",
"winapi 0.3.9",
]
[[package]]
@@ -148,7 +148,7 @@ checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
dependencies = [
"hermit-abi",
"libc",
"winapi",
"winapi 0.3.9",
]
[[package]]
@@ -163,6 +163,12 @@ version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a"
[[package]]
name = "base64"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd"
[[package]]
name = "bitflags"
version = "1.3.2"
@@ -219,12 +225,31 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c"
[[package]]
name = "cfg-if"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822"
[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "chrono"
version = "0.4.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73"
dependencies = [
"libc",
"num-integer",
"num-traits",
"time",
"winapi 0.3.9",
]
[[package]]
name = "clap"
version = "3.0.0-beta.5"
@@ -259,7 +284,7 @@ checksum = "b3616f750b84d8f0de8a58bda93e08e2a81ad3f523089b05f1dffecab48c6cbd"
dependencies = [
"atty",
"lazy_static",
"winapi",
"winapi 0.3.9",
]
[[package]]
@@ -277,7 +302,7 @@ version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "81156fece84ab6a9f2afdb109ce3ae577e42b1228441eded99bd77f627953b1a"
dependencies = [
"cfg-if",
"cfg-if 1.0.0",
]
[[package]]
@@ -286,7 +311,7 @@ version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d82cfc11ce7f2c3faef78d8a684447b40d503d9681acebed6cb728d45940c4db"
dependencies = [
"cfg-if",
"cfg-if 1.0.0",
"lazy_static",
]
@@ -414,13 +439,25 @@ dependencies = [
"instant",
]
[[package]]
name = "filetime"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "975ccf83d8d9d0d84682850a38c8169027be83368805971cc4f238c2b245bc98"
dependencies = [
"cfg-if 1.0.0",
"libc",
"redox_syscall",
"winapi 0.3.9",
]
[[package]]
name = "flate2"
version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e6988e897c1c9c485f43b47a529cef42fde0547f9d8d41a7062518f1d8fc53f"
dependencies = [
"cfg-if",
"cfg-if 1.0.0",
"crc32fast",
"libc",
"miniz_oxide",
@@ -432,12 +469,47 @@ version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
[[package]]
name = "fsevent"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ab7d1bd1bd33cc98b0889831b72da23c0aa4df9cec7e0702f46ecea04b35db6"
dependencies = [
"bitflags",
"fsevent-sys",
]
[[package]]
name = "fsevent-sys"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f41b048a94555da0f42f1d632e2e19510084fb8e303b0daa2816e733fb3644a0"
dependencies = [
"libc",
]
[[package]]
name = "fuchsia-cprng"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba"
[[package]]
name = "fuchsia-zircon"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2e9763c69ebaae630ba35f74888db465e49e259ba1bc0eda7d06f4a067615d82"
dependencies = [
"bitflags",
"fuchsia-zircon-sys",
]
[[package]]
name = "fuchsia-zircon-sys"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7"
[[package]]
name = "futures"
version = "0.3.17"
@@ -446,6 +518,7 @@ checksum = "a12aa0eb539080d55c3f2d45a67c3b58b6b0773c1a3ca2dfec66d58c97fd66ca"
dependencies = [
"futures-channel",
"futures-core",
"futures-executor",
"futures-io",
"futures-sink",
"futures-task",
@@ -468,6 +541,17 @@ version = "0.3.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "88d1c26957f23603395cd326b0ffe64124b818f4449552f960d815cfba83a53d"
[[package]]
name = "futures-executor"
version = "0.3.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "45025be030969d763025784f7f355043dc6bc74093e4ecc5000ca4dc50d8745c"
dependencies = [
"futures-core",
"futures-task",
"futures-util",
]
[[package]]
name = "futures-io"
version = "0.3.17"
@@ -508,11 +592,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "36568465210a3a6ee45e1f165136d68671471a501e632e9a98d96872222b5481"
dependencies = [
"autocfg 1.0.1",
"futures-channel",
"futures-core",
"futures-io",
"futures-sink",
"futures-task",
"memchr",
"pin-project-lite",
"pin-utils",
"slab",
]
[[package]]
@@ -521,7 +609,7 @@ version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7fcd999463524c52659517fe2cea98493cfe485d10565e7b0fb07dbba7ad2753"
dependencies = [
"cfg-if",
"cfg-if 1.0.0",
"libc",
"wasi",
]
@@ -579,13 +667,42 @@ dependencies = [
"hashbrown",
]
[[package]]
name = "inotify"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4816c66d2c8ae673df83366c18341538f234a26d65a9ecea5c348b453ac1d02f"
dependencies = [
"bitflags",
"inotify-sys",
"libc",
]
[[package]]
name = "inotify-sys"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb"
dependencies = [
"libc",
]
[[package]]
name = "instant"
version = "0.1.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c"
dependencies = [
"cfg-if",
"cfg-if 1.0.0",
]
[[package]]
name = "iovec"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b2b3ea6ff95e175473f8ffe6a7eb7c00d054240321b84c57051175fe3c1e075e"
dependencies = [
"libc",
]
[[package]]
@@ -603,6 +720,16 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "kernel32-sys"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d"
dependencies = [
"winapi 0.2.8",
"winapi-build",
]
[[package]]
name = "kv-log-macro"
version = "1.0.7"
@@ -618,12 +745,20 @@ version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
[[package]]
name = "lazycell"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55"
[[package]]
name = "lazymc"
version = "0.2.0"
version = "0.2.2"
dependencies = [
"anyhow",
"base64",
"bytes",
"chrono",
"clap",
"colored",
"derive_builder",
@@ -634,18 +769,20 @@ dependencies = [
"log",
"minecraft-protocol",
"named-binary-tag",
"notify",
"pretty_env_logger",
"quartz_nbt",
"rand 0.8.4",
"rcon",
"serde",
"serde_json",
"shlex",
"thiserror",
"tokio",
"toml",
"uuid",
"version-compare",
"winapi",
"winapi 0.3.9",
]
[[package]]
@@ -666,7 +803,7 @@ version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710"
dependencies = [
"cfg-if",
"cfg-if 1.0.0",
"value-bag",
]
@@ -685,7 +822,7 @@ checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a"
[[package]]
name = "minecraft-protocol"
version = "0.1.0"
source = "git+https://github.com/timvisee/rust-minecraft-protocol?rev=d26a525#d26a525c7b29b61d2db64805181fb5471ea4317a"
source = "git+https://github.com/timvisee/rust-minecraft-protocol?rev=356ea54#356ea5424374c5a7249be2f0f13fd3e0e2db5b58"
dependencies = [
"byteorder",
"minecraft-protocol-derive",
@@ -698,7 +835,7 @@ dependencies = [
[[package]]
name = "minecraft-protocol-derive"
version = "0.0.0"
source = "git+https://github.com/timvisee/rust-minecraft-protocol?rev=d26a525#d26a525c7b29b61d2db64805181fb5471ea4317a"
source = "git+https://github.com/timvisee/rust-minecraft-protocol?rev=356ea54#356ea5424374c5a7249be2f0f13fd3e0e2db5b58"
dependencies = [
"proc-macro2",
"quote",
@@ -715,6 +852,25 @@ dependencies = [
"autocfg 1.0.1",
]
[[package]]
name = "mio"
version = "0.6.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4afd66f5b91bf2a3bc13fad0e21caedac168ca4c707504e75585648ae80e4cc4"
dependencies = [
"cfg-if 0.1.10",
"fuchsia-zircon",
"fuchsia-zircon-sys",
"iovec",
"kernel32-sys",
"libc",
"log",
"miow 0.2.2",
"net2",
"slab",
"winapi 0.2.8",
]
[[package]]
name = "mio"
version = "0.7.14"
@@ -723,9 +879,33 @@ checksum = "8067b404fe97c70829f082dec8bcf4f71225d7eaea1d8645349cb76fa06205cc"
dependencies = [
"libc",
"log",
"miow",
"miow 0.3.7",
"ntapi",
"winapi",
"winapi 0.3.9",
]
[[package]]
name = "mio-extras"
version = "2.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52403fe290012ce777c4626790c8951324a2b9e3316b3143779c72b029742f19"
dependencies = [
"lazycell",
"log",
"mio 0.6.23",
"slab",
]
[[package]]
name = "miow"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ebd808424166322d4a38da87083bfddd3ac4c131334ed55856112eb06d46944d"
dependencies = [
"kernel32-sys",
"net2",
"winapi 0.2.8",
"ws2_32-sys",
]
[[package]]
@@ -734,7 +914,7 @@ version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9f1c5b025cda876f66ef43a113f91ebc9f4ccef34843000e0adf6ebbab84e21"
dependencies = [
"winapi",
"winapi 0.3.9",
]
[[package]]
@@ -748,13 +928,61 @@ dependencies = [
"linked-hash-map",
]
[[package]]
name = "net2"
version = "0.2.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "391630d12b68002ae1e25e8f974306474966550ad82dac6886fb8910c19568ae"
dependencies = [
"cfg-if 0.1.10",
"libc",
"winapi 0.3.9",
]
[[package]]
name = "notify"
version = "4.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae03c8c853dba7bfd23e571ff0cff7bc9dceb40a4cd684cd1681824183f45257"
dependencies = [
"bitflags",
"filetime",
"fsevent",
"fsevent-sys",
"inotify",
"libc",
"mio 0.6.23",
"mio-extras",
"walkdir",
"winapi 0.3.9",
]
[[package]]
name = "ntapi"
version = "0.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f6bb902e437b6d86e03cce10a7e2af662292c5dfef23b65899ea3ac9354ad44"
dependencies = [
"winapi",
"winapi 0.3.9",
]
[[package]]
name = "num-integer"
version = "0.1.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db"
dependencies = [
"autocfg 1.0.1",
"num-traits",
]
[[package]]
name = "num-traits"
version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290"
dependencies = [
"autocfg 1.0.1",
]
[[package]]
@@ -806,11 +1034,11 @@ version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "685404d509889fade3e86fe3a5803bca2ec09b0c0778d5ada6ec8bf7a8de5259"
dependencies = [
"cfg-if",
"cfg-if 1.0.0",
"libc",
"log",
"wepoll-ffi",
"winapi",
"winapi 0.3.9",
]
[[package]]
@@ -917,7 +1145,7 @@ dependencies = [
"rand_os",
"rand_pcg",
"rand_xorshift",
"winapi",
"winapi 0.3.9",
]
[[package]]
@@ -1011,7 +1239,7 @@ checksum = "1166d5c91dc97b88d1decc3285bb0a99ed84b05cfd0bc2341bdf2d43fc41e39b"
dependencies = [
"libc",
"rand_core 0.4.2",
"winapi",
"winapi 0.3.9",
]
[[package]]
@@ -1025,7 +1253,7 @@ dependencies = [
"libc",
"rand_core 0.4.2",
"rdrand",
"winapi",
"winapi 0.3.9",
]
[[package]]
@@ -1067,6 +1295,15 @@ dependencies = [
"rand_core 0.3.1",
]
[[package]]
name = "redox_syscall"
version = "0.2.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8383f39639269cde97d255a32bdb68c047337295414940c68bdd30c2e13203ff"
dependencies = [
"bitflags",
]
[[package]]
name = "regex"
version = "1.5.4"
@@ -1096,6 +1333,15 @@ version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e"
[[package]]
name = "same-file"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
dependencies = [
"winapi-util",
]
[[package]]
name = "serde"
version = "1.0.130"
@@ -1155,7 +1401,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5dc90fe6c7be1a323296982db1836d1ea9e47b6839496dde9a541bc496df3516"
dependencies = [
"libc",
"winapi",
"winapi 0.3.9",
]
[[package]]
@@ -1226,29 +1472,39 @@ dependencies = [
]
[[package]]
name = "tokio"
version = "1.13.0"
name = "time"
version = "0.1.43"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "588b2d10a336da58d877567cd8fb8a14b463e2104910f8132cd054b4b96e29ee"
checksum = "ca8a50ef2360fbd1eeb0ecd46795a87a19024eb4b53c5dc916ca1fd95fe62438"
dependencies = [
"libc",
"winapi 0.3.9",
]
[[package]]
name = "tokio"
version = "1.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70e992e41e0d2fb9f755b37446f20900f64446ef54874f40a60c78f021ac6144"
dependencies = [
"autocfg 1.0.1",
"bytes",
"libc",
"memchr",
"mio",
"mio 0.7.14",
"num_cpus",
"once_cell",
"pin-project-lite",
"signal-hook-registry",
"tokio-macros",
"winapi",
"winapi 0.3.9",
]
[[package]]
name = "tokio-macros"
version = "1.5.1"
version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "114383b041aa6212c579467afa0075fbbdd0718de036100bc0ba7961d8cb9095"
checksum = "c9efc1aba077437943f7515666aa2b882dfabfbfdf89c819ea75a8d6e9eaba5e"
dependencies = [
"proc-macro2",
"quote",
@@ -1324,6 +1580,17 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d5b2c62b4012a3e1eca5a7e077d13b3bf498c4073e33ccd58626607748ceeca"
[[package]]
name = "walkdir"
version = "2.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "808cf2735cd4b6866113f648b791c6adc5714537bc222d9347bb203386ffda56"
dependencies = [
"same-file",
"winapi 0.3.9",
"winapi-util",
]
[[package]]
name = "wasi"
version = "0.10.2+wasi-snapshot-preview1"
@@ -1336,7 +1603,7 @@ version = "0.2.78"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "632f73e236b219150ea279196e54e610f5dbafa5d61786303d4da54f84e47fce"
dependencies = [
"cfg-if",
"cfg-if 1.0.0",
"wasm-bindgen-macro",
]
@@ -1361,7 +1628,7 @@ version = "0.4.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e8d7523cb1f2a4c96c1317ca690031b714a51cc14e05f712446691f413f5d39"
dependencies = [
"cfg-if",
"cfg-if 1.0.0",
"js-sys",
"wasm-bindgen",
"web-sys",
@@ -1415,6 +1682,12 @@ dependencies = [
"cc",
]
[[package]]
name = "winapi"
version = "0.2.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a"
[[package]]
name = "winapi"
version = "0.3.9"
@@ -1425,6 +1698,12 @@ dependencies = [
"winapi-x86_64-pc-windows-gnu",
]
[[package]]
name = "winapi-build"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2d315eee3b34aca4797b2da6b13ed88266e6d612562a0c46390af8299fc699bc"
[[package]]
name = "winapi-i686-pc-windows-gnu"
version = "0.4.0"
@@ -1437,7 +1716,7 @@ version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178"
dependencies = [
"winapi",
"winapi 0.3.9",
]
[[package]]
@@ -1445,3 +1724,13 @@ name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "ws2_32-sys"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d59cefebd0c892fa2dd6de581e937301d8552cb44489cdff035c6187cb63fa5e"
dependencies = [
"winapi 0.2.8",
"winapi-build",
]

View File

@@ -1,6 +1,6 @@
[package]
name = "lazymc"
version = "0.2.0"
version = "0.2.2"
authors = ["Tim Visee <3a4fb3964f@sinenomine.email>"]
license = "GPL-3.0"
readme = "README.md"
@@ -32,21 +32,25 @@ lobby = ["named-binary-tag", "quartz_nbt", "uuid"]
[dependencies]
anyhow = "1.0"
base64 = "0.13"
bytes = "1.1"
chrono = "0.4"
clap = { version = "3.0.0-beta.5", default-features = false, features = [ "std", "cargo", "color", "env", "suggestions", "unicode" ]}
colored = "2.0"
derive_builder = "0.10"
dotenv = "0.15"
flate2 = { version = "1.0", default-features = false, features = ["default"] }
futures = { version = "0.3", default-features = false }
futures = { version = "0.3", default-features = false, features = ["executor"] }
log = "0.4"
minecraft-protocol = { git = "https://github.com/timvisee/rust-minecraft-protocol", rev = "d26a525" }
minecraft-protocol = { git = "https://github.com/timvisee/rust-minecraft-protocol", rev = "356ea54" }
notify = "4.0"
pretty_env_logger = "0.4"
rand = "0.8"
serde = "1.0"
serde_json = "1.0"
shlex = "1.1"
thiserror = "1.0"
tokio = { version = "1", default-features = false, features = ["rt-multi-thread", "io-util", "net", "macros", "time", "process", "signal", "sync"] }
tokio = { version = "1", default-features = false, features = ["rt-multi-thread", "io-util", "net", "macros", "time", "process", "signal", "sync", "fs"] }
toml = "0.5"
version-compare = "0.1"

View File

@@ -43,6 +43,7 @@ https://user-images.githubusercontent.com/856222/141378688-882082be-9efa-4cfe-81
- _Lobby: keep client in emulated server with lobby world, teleport to real server when ready ([experimental*](./docs/join-method-lobby.md))_
- Customizable MOTD and login messages
- Automatically manages `server.properties` (host, port and RCON settings)
- Automatically block banned IPs from server within `lazymc`
- Graceful server sleep/shutdown through RCON (with `SIGTERM` fallback on Linux/Unix)
- Restart server on crash
- Lockout mode

View File

@@ -4,6 +4,7 @@
- Resolve TODOs in code
- Don't drop errors, handle everywhere where needed (some were dropped while
prototyping to speed up development)
- Spigot/paper plugin to get real player IP from lazymc, reimplement ban-ip
## Nice to have

View File

@@ -43,6 +43,14 @@ command = "java -Xmx1G -Xms1G -jar server.jar --nogui"
#start_timeout = 300
#stop_timeout = 150
# Block banned IPs as listed in banned-ips.json in server directory.
#block_banned_ips = true
# Drop connections from banned IPs.
# Banned IPs won't be able to ping or request server status.
# On connect, clients show a 'Disconnected' message rather than the ban reason.
#drop_banned_ips = false
[time]
# Sleep after number of seconds.
#sleep_after = 60
@@ -150,4 +158,4 @@ command = "java -Xmx1G -Xms1G -jar server.jar --nogui"
[config]
# lazymc version this configuration is for.
# Don't change unless you know what you're doing.
version = "0.2.0"
version = "0.2.2"

View File

@@ -9,12 +9,13 @@ use version_compare::Cmp;
use crate::proto;
use crate::util::error::{quit_error, quit_error_msg, ErrorHintsBuilder};
use crate::util::serde::to_socket_addrs;
/// Default configuration file location.
pub const CONFIG_FILE: &str = "lazymc.toml";
/// Configuration version user should be using, or warning will be shown.
const CONFIG_VERSION: &str = "0.2.0";
const CONFIG_VERSION: &str = "0.2.1";
/// Load config from file, based on CLI arguments.
///
@@ -127,6 +128,7 @@ impl Config {
#[serde(default)]
pub struct Public {
/// Public address.
#[serde(deserialize_with = "to_socket_addrs")]
pub address: SocketAddr,
/// Minecraft protocol version name hint.
@@ -157,7 +159,10 @@ pub struct Server {
pub command: String,
/// Server address.
#[serde(default = "server_address_default")]
#[serde(
deserialize_with = "to_socket_addrs",
default = "server_address_default"
)]
pub address: SocketAddr,
/// Immediately wake server when starting lazymc.
@@ -175,6 +180,14 @@ pub struct Server {
/// Server stopping timeout. Force kill server process if it takes longer.
#[serde(default = "u32_150")]
pub stop_timeout: u32,
/// Block banned IPs as listed in banned-ips.json in server directory.
#[serde(default = "bool_true")]
pub block_banned_ips: bool,
/// Drop connections from banned IPs.
#[serde(default)]
pub drop_banned_ips: bool,
}
/// Time configuration.
@@ -318,6 +331,7 @@ impl Default for JoinHold {
#[serde(default)]
pub struct JoinForward {
/// IP and port to forward to.
#[serde(deserialize_with = "to_socket_addrs")]
pub address: SocketAddr,
}
@@ -445,3 +459,7 @@ fn u32_300() -> u32 {
fn u32_150() -> u32 {
300
}
fn bool_true() -> bool {
true
}

30
src/join/forward.rs Normal file
View File

@@ -0,0 +1,30 @@
use std::sync::Arc;
use bytes::BytesMut;
use tokio::net::TcpStream;
use crate::config::*;
use crate::service;
use super::MethodResult;
/// Forward the client.
pub async fn occupy(
config: Arc<Config>,
inbound: TcpStream,
inbound_history: &mut BytesMut,
) -> Result<MethodResult, ()> {
trace!(target: "lazymc", "Using forward method to occupy joining client");
debug!(target: "lazymc", "Forwarding client to {:?}!", config.join.forward.address);
service::server::route_proxy_address_queue(
inbound,
config.join.forward.address,
inbound_history.clone(),
);
// TODO: do not consume, continue on proxy connect failure
Ok(MethodResult::Consumed)
}

101
src/join/hold.rs Normal file
View File

@@ -0,0 +1,101 @@
use std::ops::Deref;
use std::sync::Arc;
use std::time::Duration;
use bytes::BytesMut;
use tokio::net::TcpStream;
use tokio::time;
use crate::config::*;
use crate::server::{Server, State};
use crate::service;
use super::MethodResult;
/// Hold the client.
pub async fn occupy(
config: Arc<Config>,
server: Arc<Server>,
inbound: TcpStream,
inbound_history: &mut BytesMut,
) -> Result<MethodResult, ()> {
trace!(target: "lazymc", "Using hold method to occupy joining client");
// Server must be starting
if server.state() != State::Starting {
return Ok(MethodResult::Continue(inbound));
}
// Start holding, consume client
if hold(&config, &server).await? {
service::server::route_proxy_queue(inbound, config, inbound_history.clone());
return Ok(MethodResult::Consumed);
}
Ok(MethodResult::Continue(inbound))
}
/// Hold a client while server starts.
///
/// Returns holding status. `true` if client is held and it should be proxied, `false` it was held
/// but it timed out.
async fn hold<'a>(config: &Config, server: &Server) -> Result<bool, ()> {
trace!(target: "lazymc", "Started holding client");
// A task to wait for suitable server state
// Waits for started state, errors if stopping/stopped state is reached
let task_wait = async {
let mut state = server.state_receiver();
loop {
// Wait for state change
state.changed().await.unwrap();
match state.borrow().deref() {
// Still waiting on server start
State::Starting => {
trace!(target: "lazymc", "Server not ready, holding client for longer");
continue;
}
// Server started, start relaying and proxy
State::Started => {
break true;
}
// Server stopping, this shouldn't happen, kick
State::Stopping => {
warn!(target: "lazymc", "Server stopping for held client, disconnecting");
break false;
}
// Server stopped, this shouldn't happen, disconnect
State::Stopped => {
error!(target: "lazymc", "Server stopped for held client, disconnecting");
break false;
}
}
}
};
// Wait for server state with timeout
let timeout = Duration::from_secs(config.join.hold.timeout as u64);
match time::timeout(timeout, task_wait).await {
// Relay client to proxy
Ok(true) => {
info!(target: "lazymc", "Server ready for held client, relaying to server");
Ok(true)
}
// Server stopping/stopped, this shouldn't happen, kick
Ok(false) => {
warn!(target: "lazymc", "Server stopping for held client");
Ok(false)
}
// Timeout reached, kick with starting message
Err(_) => {
warn!(target: "lazymc", "Held client reached timeout of {}s", config.join.hold.timeout);
Ok(false)
}
}
}

33
src/join/kick.rs Normal file
View File

@@ -0,0 +1,33 @@
use tokio::net::TcpStream;
use crate::config::*;
use crate::net;
use crate::proto::action;
use crate::proto::client::Client;
use crate::server::{self, Server};
use super::MethodResult;
/// Kick the client.
pub async fn occupy(
client: &Client,
config: &Config,
server: &Server,
mut inbound: TcpStream,
) -> Result<MethodResult, ()> {
trace!(target: "lazymc", "Using kick method to occupy joining client");
// Select message and kick
let msg = match server.state() {
server::State::Starting | server::State::Stopped | server::State::Started => {
&config.join.kick.starting
}
server::State::Stopping => &config.join.kick.stopping,
};
action::kick(client, msg, &mut inbound.split().1).await?;
// Gracefully close connection
net::close_tcp_stream(inbound).await.map_err(|_| ())?;
Ok(MethodResult::Consumed)
}

30
src/join/lobby.rs Normal file
View File

@@ -0,0 +1,30 @@
use std::sync::Arc;
use bytes::BytesMut;
use tokio::net::TcpStream;
use crate::config::*;
use crate::lobby;
use crate::proto::client::{Client, ClientInfo};
use crate::server::Server;
use super::MethodResult;
/// Lobby the client.
pub async fn occupy(
client: &Client,
client_info: ClientInfo,
config: Arc<Config>,
server: Arc<Server>,
inbound: TcpStream,
inbound_queue: BytesMut,
) -> Result<MethodResult, ()> {
trace!(target: "lazymc", "Using lobby method to occupy joining client");
// Start lobby
lobby::serve(client, client_info, inbound, config, server, inbound_queue).await?;
// TODO: do not consume client here, allow other join method on fail
Ok(MethodResult::Consumed)
}

106
src/join/mod.rs Normal file
View File

@@ -0,0 +1,106 @@
use std::sync::Arc;
use bytes::BytesMut;
use tokio::net::TcpStream;
use crate::config::*;
use crate::net;
use crate::proto::client::{Client, ClientInfo, ClientState};
use crate::server::Server;
pub mod forward;
pub mod hold;
pub mod kick;
#[cfg(feature = "lobby")]
pub mod lobby;
/// A result returned by a join occupy method.
pub enum MethodResult {
/// Client is consumed.
Consumed,
/// Method is done, continue with the next.
Continue(TcpStream),
}
/// Start occupying client.
///
/// This assumes the login start packet has just been received.
pub async fn occupy(
client: Client,
#[allow(unused_variables)] client_info: ClientInfo,
config: Arc<Config>,
server: Arc<Server>,
mut inbound: TcpStream,
mut inbound_history: BytesMut,
#[allow(unused_variables)] login_queue: BytesMut,
) -> Result<(), ()> {
// Assert state is correct
assert_eq!(
client.state(),
ClientState::Login,
"when occupying client, it should be in login state"
);
// Go through all configured join methods
for method in &config.join.methods {
// Invoke method, take result
let result = match method {
// Kick method, immediately kick client
Method::Kick => kick::occupy(&client, &config, &server, inbound).await?,
// Hold method, hold client connection while server starts
Method::Hold => {
hold::occupy(
config.clone(),
server.clone(),
inbound,
&mut inbound_history,
)
.await?
}
// Forward method, forward client connection while server starts
Method::Forward => {
forward::occupy(config.clone(), inbound, &mut inbound_history).await?
}
// Lobby method, keep client in lobby while server starts
#[cfg(feature = "lobby")]
Method::Lobby => {
lobby::occupy(
&client,
client_info.clone(),
config.clone(),
server.clone(),
inbound,
login_queue.clone(),
)
.await?
}
// Lobby method, keep client in lobby while server starts
#[cfg(not(feature = "lobby"))]
Method::Lobby => {
error!(target: "lazymc", "Lobby join method not supported in this lazymc build");
MethodResult::Continue(inbound)
}
};
// Handle method result
match result {
MethodResult::Consumed => return Ok(()),
MethodResult::Continue(stream) => {
inbound = stream;
continue;
}
}
}
debug!(target: "lazymc", "No method left to occupy joining client, disconnecting");
// Gracefully close connection
net::close_tcp_stream(inbound).await.map_err(|_| ())?;
Ok(())
}

View File

@@ -8,15 +8,13 @@ use bytes::BytesMut;
use futures::FutureExt;
use minecraft_protocol::data::chat::{Message, Payload};
use minecraft_protocol::decoder::Decoder;
use minecraft_protocol::encoder::Encoder;
use minecraft_protocol::version::v1_14_4::handshake::Handshake;
use minecraft_protocol::version::v1_14_4::login::{LoginStart, LoginSuccess, SetCompression};
use minecraft_protocol::version::v1_17_1::game::{
ClientBoundKeepAlive, JoinGame, NamedSoundEffect, PlayerPositionAndLook, PluginMessage,
Respawn, SetTitleSubtitle, SetTitleText, SetTitleTimes, TimeUpdate,
ClientBoundKeepAlive, ClientBoundPluginMessage, JoinGame, NamedSoundEffect,
PlayerPositionAndLook, Respawn, SetTitleSubtitle, SetTitleText, SetTitleTimes, TimeUpdate,
};
use nbt::CompoundTag;
use tokio::io::{self, AsyncWriteExt};
use tokio::net::tcp::{ReadHalf, WriteHalf};
use tokio::net::TcpStream;
use tokio::select;
@@ -25,7 +23,10 @@ use uuid::Uuid;
use crate::config::*;
use crate::mc;
use crate::proto::{self, Client, ClientInfo, ClientState, RawPacket};
use crate::net;
use crate::proto;
use crate::proto::client::{Client, ClientInfo, ClientState};
use crate::proto::{packet, packets};
use crate::proxy;
use crate::server::{Server, State};
@@ -62,7 +63,7 @@ const SERVER_BRAND: &[u8] = b"lazymc";
// TODO: do not drop error here, return Box<dyn Error>
// TODO: on error, nicely kick client with message
pub async fn serve(
client: Client,
client: &Client,
client_info: ClientInfo,
mut inbound: TcpStream,
config: Arc<Config>,
@@ -88,7 +89,7 @@ pub async fn serve(
loop {
// Read packet from stream
let (packet, _raw) = match proto::read_packet(&client, &mut inbound_buf, &mut reader).await
let (packet, _raw) = match packet::read_packet(client, &mut inbound_buf, &mut reader).await
{
Ok(Some(packet)) => packet,
Ok(None) => break,
@@ -102,9 +103,7 @@ pub async fn serve(
let client_state = client.state();
// Hijack login start
if client_state == ClientState::Login
&& packet.id == proto::packets::login::SERVER_LOGIN_START
{
if client_state == ClientState::Login && packet.id == packets::login::SERVER_LOGIN_START {
// Parse login start packet
let login_start = LoginStart::decode(&mut packet.data.as_slice()).map_err(|_| ())?;
@@ -113,21 +112,21 @@ pub async fn serve(
// Respond with set compression if compression is enabled based on threshold
if proto::COMPRESSION_THRESHOLD >= 0 {
trace!(target: "lazymc::lobby", "Enabling compression for lobby client because server has it enabled (threshold: {})", proto::COMPRESSION_THRESHOLD);
respond_set_compression(&client, &mut writer, proto::COMPRESSION_THRESHOLD).await?;
respond_set_compression(client, &mut writer, proto::COMPRESSION_THRESHOLD).await?;
client.set_compression(proto::COMPRESSION_THRESHOLD);
}
// Respond with login success, switch to play state
respond_login_success(&client, &mut writer, &login_start).await?;
respond_login_success(client, &mut writer, &login_start).await?;
client.set_state(ClientState::Play);
trace!(target: "lazymc::lobby", "Client login success, sending required play packets for lobby world");
// Send packets to client required to get into workable play state for lobby world
send_lobby_play_packets(&client, &mut writer, &server).await?;
send_lobby_play_packets(client, &mut writer, &server).await?;
// Wait for server to come online, then set up new connection to it
stage_wait(&client, &server, &config, &mut writer).await?;
stage_wait(client, &server, &config, &mut writer).await?;
let (server_client, mut outbound, mut server_buf) =
connect_to_server(client_info, &config).await?;
@@ -136,10 +135,10 @@ pub async fn serve(
wait_for_server_join_game(&server_client, &mut outbound, &mut server_buf).await?;
// Reset lobby title
send_lobby_title(&client, &mut writer, "").await?;
send_lobby_title(client, &mut writer, "").await?;
// Play ready sound if configured
play_lobby_ready_sound(&client, &mut writer, &config).await?;
play_lobby_ready_sound(client, &mut writer, &config).await?;
// Wait a second because Notchian servers are slow
// See: https://wiki.vg/Protocol#Login_Success
@@ -147,7 +146,7 @@ pub async fn serve(
time::sleep(SERVER_WARMUP).await;
// Send respawn packet, initiates teleport to real server world
send_respawn_from_join(&client, &mut writer, join_game).await?;
send_respawn_from_join(client, &mut writer, join_game).await?;
// Drain inbound connection so we don't confuse the server
// TODO: can we void everything? we might need to forward everything to server except
@@ -172,11 +171,7 @@ pub async fn serve(
}
// Gracefully close connection
match writer.shutdown().await {
Ok(_) => {}
Err(err) if err.kind() == io::ErrorKind::NotConnected => {}
Err(_) => return Err(()),
}
net::close_tcp_stream(inbound).await.map_err(|_| ())?;
Ok(())
}
@@ -187,16 +182,7 @@ async fn respond_set_compression(
writer: &mut WriteHalf<'_>,
threshold: i32,
) -> Result<(), ()> {
let packet = SetCompression { threshold };
let mut data = Vec::new();
packet.encode(&mut data).map_err(|_| ())?;
let response =
RawPacket::new(proto::packets::login::CLIENT_SET_COMPRESSION, data).encode(client)?;
writer.write_all(&response).await.map_err(|_| ())?;
Ok(())
packet::write_packet(SetCompression { threshold }, client, writer).await
}
/// Respond to client with login success packet
@@ -206,22 +192,18 @@ async fn respond_login_success(
writer: &mut WriteHalf<'_>,
login_start: &LoginStart,
) -> Result<(), ()> {
let packet = LoginSuccess {
packet::write_packet(
LoginSuccess {
uuid: Uuid::new_v3(
&Uuid::new_v3(&Uuid::nil(), b"OfflinePlayer"),
login_start.name.as_bytes(),
),
username: login_start.name.clone(),
};
let mut data = Vec::new();
packet.encode(&mut data).map_err(|_| ())?;
let response =
RawPacket::new(proto::packets::login::CLIENT_LOGIN_SUCCESS, data).encode(client)?;
writer.write_all(&response).await.map_err(|_| ())?;
Ok(())
},
client,
writer,
)
.await
}
/// Play lobby ready sound effect if configured.
@@ -275,7 +257,8 @@ async fn send_lobby_join_game(
server: &Server,
) -> Result<(), ()> {
// Send Minecrafts default states, slightly customised for lobby world
let packet = {
packet::write_packet(
{
let status = server.status().await;
JoinGame {
@@ -305,38 +288,31 @@ async fn send_lobby_join_game(
is_debug: true,
is_flat: false,
}
};
let mut data = Vec::new();
packet.encode(&mut data).map_err(|_| ())?;
let response = RawPacket::new(proto::packets::play::CLIENT_JOIN_GAME, data).encode(client)?;
writer.write_all(&response).await.map_err(|_| ())?;
Ok(())
},
client,
writer,
)
.await
}
/// Send lobby brand to client.
async fn send_lobby_brand(client: &Client, writer: &mut WriteHalf<'_>) -> Result<(), ()> {
let packet = PluginMessage {
packet::write_packet(
ClientBoundPluginMessage {
channel: "minecraft:brand".into(),
data: SERVER_BRAND.into(),
};
let mut data = Vec::new();
packet.encode(&mut data).map_err(|_| ())?;
let response =
RawPacket::new(proto::packets::play::CLIENT_PLUGIN_MESSAGE, data).encode(client)?;
writer.write_all(&response).await.map_err(|_| ())?;
Ok(())
},
client,
writer,
)
.await
}
/// Send lobby player position to client.
async fn send_lobby_player_pos(client: &Client, writer: &mut WriteHalf<'_>) -> Result<(), ()> {
// Send player location, disables download terrain screen
let packet = PlayerPositionAndLook {
packet::write_packet(
PlayerPositionAndLook {
x: 0.0,
y: 0.0,
z: 0.0,
@@ -345,16 +321,11 @@ async fn send_lobby_player_pos(client: &Client, writer: &mut WriteHalf<'_>) -> R
flags: 0b00000000,
teleport_id: 0,
dismount_vehicle: true,
};
let mut data = Vec::new();
packet.encode(&mut data).map_err(|_| ())?;
let response =
RawPacket::new(proto::packets::play::CLIENT_PLAYER_POS_LOOK, data).encode(client)?;
writer.write_all(&response).await.map_err(|_| ())?;
Ok(())
},
client,
writer,
)
.await
}
/// Send lobby time update to client.
@@ -362,38 +333,32 @@ async fn send_lobby_time_update(client: &Client, writer: &mut WriteHalf<'_>) ->
const MC_TIME_NOON: i64 = 6000;
// Send time update, required once for keep-alive packets
let packet = TimeUpdate {
packet::write_packet(
TimeUpdate {
world_age: MC_TIME_NOON,
time_of_day: MC_TIME_NOON,
};
let mut data = Vec::new();
packet.encode(&mut data).map_err(|_| ())?;
let response = RawPacket::new(proto::packets::play::CLIENT_TIME_UPDATE, data).encode(client)?;
writer.write_all(&response).await.map_err(|_| ())?;
Ok(())
},
client,
writer,
)
.await
}
/// Send keep alive packet to client.
///
/// Required periodically in play mode to prevent client timeout.
async fn send_keep_alive(client: &Client, writer: &mut WriteHalf<'_>) -> Result<(), ()> {
let packet = ClientBoundKeepAlive {
packet::write_packet(
ClientBoundKeepAlive {
// Keep sending new IDs
id: KEEP_ALIVE_ID.fetch_add(1, Ordering::Relaxed),
};
let mut data = Vec::new();
packet.encode(&mut data).map_err(|_| ())?;
let response = RawPacket::new(proto::packets::play::CLIENT_KEEP_ALIVE, data).encode(client)?;
writer.write_all(&response).await.map_err(|_| ())?;
},
client,
writer,
)
.await
// TODO: verify we receive keep alive response with same ID from client
Ok(())
}
/// Send lobby title packets to client.
@@ -411,31 +376,28 @@ async fn send_lobby_title(
let subtitle = text.lines().skip(1).collect::<Vec<_>>().join("\n");
// Set title
let packet = SetTitleText {
packet::write_packet(
SetTitleText {
text: Message::new(Payload::text(title)),
};
let mut data = Vec::new();
packet.encode(&mut data).map_err(|_| ())?;
let response =
RawPacket::new(proto::packets::play::CLIENT_SET_TITLE_TEXT, data).encode(client)?;
writer.write_all(&response).await.map_err(|_| ())?;
},
client,
writer,
)
.await?;
// Set subtitle
let packet = SetTitleSubtitle {
packet::write_packet(
SetTitleSubtitle {
text: Message::new(Payload::text(&subtitle)),
};
let mut data = Vec::new();
packet.encode(&mut data).map_err(|_| ())?;
let response =
RawPacket::new(proto::packets::play::CLIENT_SET_TITLE_SUBTITLE, data).encode(client)?;
writer.write_all(&response).await.map_err(|_| ())?;
},
client,
writer,
)
.await?;
// Set title times
let packet = if title.is_empty() && subtitle.is_empty() {
packet::write_packet(
if title.is_empty() && subtitle.is_empty() {
// Defaults: https://minecraft.fandom.com/wiki/Commands/title#Detail
SetTitleTimes {
fade_in: 10,
@@ -448,16 +410,11 @@ async fn send_lobby_title(
stay: KEEP_ALIVE_INTERVAL.as_secs() as i32 * mc::TICKS_PER_SECOND as i32 * 2,
fade_out: 0,
}
};
let mut data = Vec::new();
packet.encode(&mut data).map_err(|_| ())?;
let response =
RawPacket::new(proto::packets::play::CLIENT_SET_TITLE_TIMES, data).encode(client)?;
writer.write_all(&response).await.map_err(|_| ())?;
Ok(())
},
client,
writer,
)
.await
}
/// Send lobby ready sound effect to client.
@@ -466,7 +423,8 @@ async fn send_lobby_sound_effect(
writer: &mut WriteHalf<'_>,
sound_name: &str,
) -> Result<(), ()> {
let packet = NamedSoundEffect {
packet::write_packet(
NamedSoundEffect {
sound_name: sound_name.into(),
sound_category: 0,
effect_pos_x: 0,
@@ -474,16 +432,11 @@ async fn send_lobby_sound_effect(
effect_pos_z: 0,
volume: 1.0,
pitch: 1.0,
};
let mut data = Vec::new();
packet.encode(&mut data).map_err(|_| ())?;
let response =
RawPacket::new(proto::packets::play::CLIENT_NAMED_SOUND_EFFECT, data).encode(client)?;
writer.write_all(&response).await.map_err(|_| ())?;
Ok(())
},
client,
writer,
)
.await
}
/// Send respawn packet to client to jump from lobby into now loaded server.
@@ -494,7 +447,8 @@ async fn send_respawn_from_join(
writer: &mut WriteHalf<'_>,
join_game: JoinGame,
) -> Result<(), ()> {
let packet = Respawn {
packet::write_packet(
Respawn {
dimension: join_game.dimension,
world_name: join_game.world_name,
hashed_seed: join_game.hashed_seed,
@@ -503,15 +457,11 @@ async fn send_respawn_from_join(
is_debug: join_game.is_debug,
is_flat: join_game.is_flat,
copy_metadata: false,
};
let mut data = Vec::new();
packet.encode(&mut data).map_err(|_| ())?;
let response = RawPacket::new(proto::packets::play::CLIENT_RESPAWN, data).encode(client)?;
writer.write_all(&response).await.map_err(|_| ())?;
Ok(())
},
client,
writer,
)
.await
}
/// An infinite keep-alive loop.
@@ -638,44 +588,44 @@ async fn connect_to_server_no_timeout(
.await
.map_err(|_| ())?;
let (mut reader, mut writer) = outbound.split();
let tmp_client = Client::default();
// Construct temporary server client
let tmp_client = match outbound.local_addr() {
Ok(addr) => Client::new(addr),
Err(_) => Client::dummy(),
};
tmp_client.set_state(ClientState::Login);
let (mut reader, mut writer) = outbound.split();
// Handshake packet
let packet = Handshake {
packet::write_packet(
Handshake {
protocol_version: client_info.protocol_version.unwrap(),
server_addr: config.server.address.ip().to_string(),
server_port: config.server.address.port(),
next_state: ClientState::Login.to_id(),
};
let mut data = Vec::new();
packet.encode(&mut data).map_err(|_| ())?;
let request =
RawPacket::new(proto::packets::handshake::SERVER_HANDSHAKE, data).encode(&tmp_client)?;
writer.write_all(&request).await.map_err(|_| ())?;
},
&tmp_client,
&mut writer,
)
.await?;
// Request login start
let packet = LoginStart {
packet::write_packet(
LoginStart {
name: client_info.username.ok_or(())?,
};
let mut data = Vec::new();
packet.encode(&mut data).map_err(|_| ())?;
let request =
RawPacket::new(proto::packets::login::SERVER_LOGIN_START, data).encode(&tmp_client)?;
writer.write_all(&request).await.map_err(|_| ())?;
},
&tmp_client,
&mut writer,
)
.await?;
// Incoming buffer
let mut buf = BytesMut::new();
loop {
// Read packet from stream
let (packet, _raw) = match proto::read_packet(&tmp_client, &mut buf, &mut reader).await {
let (packet, _raw) = match packet::read_packet(&tmp_client, &mut buf, &mut reader).await {
Ok(Some(packet)) => packet,
Ok(None) => break,
Err(_) => {
@@ -688,8 +638,7 @@ async fn connect_to_server_no_timeout(
let client_state = tmp_client.state();
// Catch set compression
if client_state == ClientState::Login
&& packet.id == proto::packets::login::CLIENT_SET_COMPRESSION
if client_state == ClientState::Login && packet.id == packets::login::CLIENT_SET_COMPRESSION
{
// Decode compression packet
let set_compression =
@@ -713,9 +662,7 @@ async fn connect_to_server_no_timeout(
}
// Hijack login success
if client_state == ClientState::Login
&& packet.id == proto::packets::login::CLIENT_LOGIN_SUCCESS
{
if client_state == ClientState::Login && packet.id == packets::login::CLIENT_LOGIN_SUCCESS {
trace!(target: "lazymc::lobby", "Received login success from server connection, change to play mode");
// TODO: parse this packet to ensure it's fine
@@ -743,11 +690,7 @@ async fn connect_to_server_no_timeout(
}
// Gracefully close connection
match writer.shutdown().await {
Ok(_) => {}
Err(err) if err.kind() == io::ErrorKind::NotConnected => {}
Err(_) => return Err(()),
}
net::close_tcp_stream(outbound).await.map_err(|_| ())?;
Err(())
}
@@ -780,11 +723,11 @@ async fn wait_for_server_join_game_no_timeout(
outbound: &mut TcpStream,
buf: &mut BytesMut,
) -> Result<JoinGame, ()> {
let (mut reader, mut writer) = outbound.split();
let (mut reader, mut _writer) = outbound.split();
loop {
// Read packet from stream
let (packet, _raw) = match proto::read_packet(client, buf, &mut reader).await {
let (packet, _raw) = match packet::read_packet(client, buf, &mut reader).await {
Ok(Some(packet)) => packet,
Ok(None) => break,
Err(_) => {
@@ -794,7 +737,7 @@ async fn wait_for_server_join_game_no_timeout(
};
// Catch join game
if packet.id == proto::packets::play::CLIENT_JOIN_GAME {
if packet.id == packets::play::CLIENT_JOIN_GAME {
let join_game = JoinGame::decode(&mut packet.data.as_slice()).map_err(|err| {
dbg!(err);
})?;
@@ -808,11 +751,7 @@ async fn wait_for_server_join_game_no_timeout(
}
// Gracefully close connection
match writer.shutdown().await {
Ok(_) => {}
Err(err) if err.kind() == io::ErrorKind::NotConnected => {}
Err(_) => return Err(()),
}
net::close_tcp_stream_ref(outbound).await.map_err(|_| ())?;
Err(())
}

View File

@@ -10,10 +10,12 @@ extern crate log;
pub(crate) mod action;
pub(crate) mod cli;
pub(crate) mod config;
pub(crate) mod join;
#[cfg(feature = "lobby")]
pub(crate) mod lobby;
pub(crate) mod mc;
pub(crate) mod monitor;
pub(crate) mod net;
pub(crate) mod os;
pub(crate) mod proto;
pub(crate) mod proxy;

99
src/mc/ban.rs Normal file
View File

@@ -0,0 +1,99 @@
use std::collections::HashMap;
use std::error::Error;
use std::fs;
use std::net::IpAddr;
use std::path::Path;
use chrono::{DateTime, Utc};
use serde::Deserialize;
/// File name.
pub const FILE: &str = "banned-ips.json";
/// The forever expiry literal.
const EXPIRY_FOREVER: &str = "forever";
/// List of banned IPs.
#[derive(Debug, Default)]
pub struct BannedIps {
/// List of banned IPs.
ips: HashMap<IpAddr, BannedIp>,
}
impl BannedIps {
/// Get ban entry if IP if it exists.
///
/// This uses the latest known `banned-ips.json` contents if known.
/// If this feature is disabled, this will always return false.
pub fn get(&self, ip: &IpAddr) -> Option<BannedIp> {
self.ips.get(ip).cloned()
}
/// Check whether the given IP is banned.
///
/// This uses the latest known `banned-ips.json` contents if known.
/// If this feature is disabled, this will always return false.
pub fn is_banned(&self, ip: &IpAddr) -> bool {
self.ips.get(ip).map(|ip| ip.is_banned()).unwrap_or(false)
}
}
/// A banned IP entry.
#[derive(Debug, Deserialize, Clone)]
pub struct BannedIp {
/// Banned IP.
pub ip: IpAddr,
/// Ban creation time.
pub created: Option<String>,
/// Ban source.
pub source: Option<String>,
/// Ban expiry time.
pub expires: Option<String>,
/// Ban reason.
pub reason: Option<String>,
}
impl BannedIp {
/// Check if this entry is currently banned.
pub fn is_banned(&self) -> bool {
// Get expiry time
let expires = match &self.expires {
Some(expires) => expires,
None => return true,
};
// If expiry is forever, the user is banned
if expires.trim().to_lowercase() == EXPIRY_FOREVER {
return true;
}
// Parse expiry time, check if it has passed
let expiry = match DateTime::parse_from_str(expires, "%Y-%m-%d %H:%M:%S %z") {
Ok(expiry) => expiry,
Err(err) => {
error!(target: "lazymc", "Failed to parse ban expiry '{}', assuming still banned: {}", expires, err);
return true;
}
};
expiry > Utc::now()
}
}
/// Load banned IPs from file.
pub fn load(path: &Path) -> Result<BannedIps, Box<dyn Error>> {
// Load file contents
let contents = fs::read_to_string(path)?;
// Parse contents
let ips: Vec<BannedIp> = serde_json::from_str(&contents)?;
debug!(target: "lazymc", "Loaded {} banned IPs", ips.len());
// Transform into map
let ips = ips.into_iter().map(|ip| (ip.ip, ip)).collect();
Ok(BannedIps { ips })
}

View File

@@ -1,3 +1,4 @@
pub mod ban;
#[cfg(feature = "rcon")]
pub mod rcon;
pub mod server_properties;

View File

@@ -7,16 +7,17 @@ use std::time::Duration;
use bytes::BytesMut;
use minecraft_protocol::data::server_status::ServerStatus;
use minecraft_protocol::decoder::Decoder;
use minecraft_protocol::encoder::Encoder;
use minecraft_protocol::version::v1_14_4::handshake::Handshake;
use minecraft_protocol::version::v1_14_4::status::{PingRequest, PingResponse, StatusResponse};
use minecraft_protocol::version::v1_14_4::status::{
PingRequest, PingResponse, StatusRequest, StatusResponse,
};
use rand::Rng;
use tokio::io::AsyncWriteExt;
use tokio::net::TcpStream;
use tokio::time;
use crate::config::Config;
use crate::proto::{self, Client, ClientState, RawPacket};
use crate::proto::client::{Client, ClientState};
use crate::proto::{packet, packets};
use crate::server::{Server, State};
/// Monitor ping inverval in seconds.
@@ -97,7 +98,7 @@ async fn fetch_status(config: &Config, addr: SocketAddr) -> Result<ServerStatus,
let mut stream = TcpStream::connect(addr).await.map_err(|_| ())?;
// Dummy client
let client = Client::default();
let client = Client::dummy();
send_handshake(&client, &mut stream, config, addr).await?;
request_status(&client, &mut stream).await?;
@@ -109,7 +110,7 @@ async fn do_ping(config: &Config, addr: SocketAddr) -> Result<(), ()> {
let mut stream = TcpStream::connect(addr).await.map_err(|_| ())?;
// Dummy client
let client = Client::default();
let client = Client::dummy();
send_handshake(&client, &mut stream, config, addr).await?;
let token = send_ping(&client, &mut stream).await?;
@@ -123,45 +124,28 @@ async fn send_handshake(
config: &Config,
addr: SocketAddr,
) -> Result<(), ()> {
let handshake = Handshake {
packet::write_packet(
Handshake {
protocol_version: config.public.protocol as i32,
server_addr: addr.ip().to_string(),
server_port: addr.port(),
next_state: ClientState::Status.to_id(),
};
let mut packet = Vec::new();
handshake.encode(&mut packet).map_err(|_| ())?;
let raw = RawPacket::new(proto::packets::handshake::SERVER_HANDSHAKE, packet)
.encode(client)
.map_err(|_| ())?;
stream.write_all(&raw).await.map_err(|_| ())?;
Ok(())
},
client,
&mut stream.split().1,
)
.await
}
/// Send status request.
async fn request_status(client: &Client, stream: &mut TcpStream) -> Result<(), ()> {
let raw = RawPacket::new(proto::packets::status::SERVER_STATUS, vec![])
.encode(client)
.map_err(|_| ())?;
stream.write_all(&raw).await.map_err(|_| ())?;
Ok(())
packet::write_packet(StatusRequest {}, client, &mut stream.split().1).await
}
/// Send status request.
async fn send_ping(client: &Client, stream: &mut TcpStream) -> Result<u64, ()> {
let token = rand::thread_rng().gen();
let ping = PingRequest { time: token };
let mut packet = Vec::new();
ping.encode(&mut packet).map_err(|_| ())?;
let raw = RawPacket::new(proto::packets::status::SERVER_PING, packet)
.encode(client)
.map_err(|_| ())?;
stream.write_all(&raw).await.map_err(|_| ())?;
packet::write_packet(PingRequest { time: token }, client, &mut stream.split().1).await?;
Ok(token)
}
@@ -173,14 +157,14 @@ async fn wait_for_status(client: &Client, stream: &mut TcpStream) -> Result<Serv
loop {
// Read packet from stream
let (packet, _raw) = match proto::read_packet(client, &mut buf, &mut reader).await {
let (packet, _raw) = match packet::read_packet(client, &mut buf, &mut reader).await {
Ok(Some(packet)) => packet,
Ok(None) => break,
Err(_) => continue,
};
// Catch status response
if packet.id == proto::packets::status::CLIENT_STATUS {
if packet.id == packets::status::CLIENT_STATUS {
let status = StatusResponse::decode(&mut packet.data.as_slice()).map_err(|_| ())?;
return Ok(status.server_status);
}
@@ -209,14 +193,14 @@ async fn wait_for_ping(client: &Client, stream: &mut TcpStream, token: u64) -> R
loop {
// Read packet from stream
let (packet, _raw) = match proto::read_packet(client, &mut buf, &mut reader).await {
let (packet, _raw) = match packet::read_packet(client, &mut buf, &mut reader).await {
Ok(Some(packet)) => packet,
Ok(None) => break,
Err(_) => continue,
};
// Catch ping response
if packet.id == proto::packets::status::CLIENT_PING {
if packet.id == packets::status::CLIENT_PING {
let ping = PingResponse::decode(&mut packet.data.as_slice()).map_err(|_| ())?;
// Ping token must match

22
src/net.rs Normal file
View File

@@ -0,0 +1,22 @@
use std::error::Error;
use std::io;
use tokio::io::AsyncWriteExt;
use tokio::net::TcpStream;
/// Gracefully close given TCP stream.
///
/// Intended as helper to make code less messy. This also succeeds if already closed.
pub async fn close_tcp_stream(mut stream: TcpStream) -> Result<(), Box<dyn Error>> {
close_tcp_stream_ref(&mut stream).await
}
/// Gracefully close given TCP stream.
///
/// Intended as helper to make code less messy. This also succeeds if already closed.
pub async fn close_tcp_stream_ref(stream: &mut TcpStream) -> Result<(), Box<dyn Error>> {
match stream.shutdown().await {
Ok(_) => Ok(()),
Err(err) if err.kind() == io::ErrorKind::NotConnected => Ok(()),
Err(err) => Err(err.into()),
}
}

36
src/proto/action.rs Normal file
View File

@@ -0,0 +1,36 @@
use minecraft_protocol::data::chat::{Message, Payload};
use minecraft_protocol::version::v1_14_4::game::GameDisconnect;
use minecraft_protocol::version::v1_14_4::login::LoginDisconnect;
use tokio::net::tcp::WriteHalf;
use crate::proto::client::{Client, ClientState};
use crate::proto::packet;
/// Kick client with a message.
///
/// Should close connection afterwards.
pub async fn kick(client: &Client, msg: &str, writer: &mut WriteHalf<'_>) -> Result<(), ()> {
match client.state() {
ClientState::Login => {
packet::write_packet(
LoginDisconnect {
reason: Message::new(Payload::text(msg)),
},
client,
writer,
)
.await
}
ClientState::Play => {
packet::write_packet(
GameDisconnect {
reason: Message::new(Payload::text(msg)),
},
client,
writer,
)
.await
}
_ => Err(()),
}
}

127
src/proto/client.rs Normal file
View File

@@ -0,0 +1,127 @@
use std::net::SocketAddr;
use std::sync::atomic::{AtomicI32, Ordering};
use std::sync::Mutex;
/// Client state.
///
/// Note: this does not keep track of encryption states.
#[derive(Debug)]
pub struct Client {
/// Client peer address.
pub peer: SocketAddr,
/// Current client state.
pub state: Mutex<ClientState>,
/// Compression state.
///
/// 0 or positive if enabled, negative if disabled.
pub compression: AtomicI32,
}
impl Client {
/// Construct new client with given peer address.
pub fn new(peer: SocketAddr) -> Self {
Self {
peer,
state: Default::default(),
compression: AtomicI32::new(-1),
}
}
/// Construct dummy client.
pub fn dummy() -> Self {
Self::new("0.0.0.0:0".parse().unwrap())
}
/// Get client state.
pub fn state(&self) -> ClientState {
*self.state.lock().unwrap()
}
/// Set client state.
pub fn set_state(&self, state: ClientState) {
*self.state.lock().unwrap() = state;
}
/// Get compression threshold.
pub fn compressed(&self) -> i32 {
self.compression.load(Ordering::Relaxed)
}
/// Whether compression is used.
pub fn is_compressed(&self) -> bool {
self.compressed() >= 0
}
/// Set compression value.
#[allow(unused)]
pub fn set_compression(&self, threshold: i32) {
trace!(target: "lazymc", "Client now uses compression threshold of {}", threshold);
self.compression.store(threshold, Ordering::Relaxed);
}
}
/// Protocol state a client may be in.
///
/// Note: this does not include the `play` state, because this is never used anymore when a client
/// reaches this state.
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub enum ClientState {
/// Initial client state.
Handshake,
/// State to query server status.
Status,
/// State to login to server.
Login,
/// State to play on the server.
#[allow(unused)]
Play,
}
impl ClientState {
/// From state ID.
pub fn from_id(id: i32) -> Option<Self> {
match id {
0 => Some(Self::Handshake),
1 => Some(Self::Status),
2 => Some(Self::Login),
_ => None,
}
}
/// Get state ID.
pub fn to_id(self) -> i32 {
match self {
Self::Handshake => 0,
Self::Status => 1,
Self::Login => 2,
Self::Play => -1,
}
}
}
impl Default for ClientState {
fn default() -> Self {
Self::Handshake
}
}
/// Client info, useful during connection handling.
#[derive(Debug, Clone, Default)]
pub struct ClientInfo {
/// Client protocol version.
pub protocol_version: Option<i32>,
/// Client username.
pub username: Option<String>,
}
impl ClientInfo {
pub fn empty() -> Self {
Self::default()
}
}

27
src/proto/mod.rs Normal file
View File

@@ -0,0 +1,27 @@
pub mod action;
pub mod client;
pub mod packet;
pub mod packets;
/// Default minecraft protocol version name.
///
/// Just something to default to when real server version isn't known or when no hint is specified
/// in the configuration.
///
/// Should be kept up-to-date with latest supported Minecraft version by lazymc.
pub const PROTO_DEFAULT_VERSION: &str = "1.17.1";
/// Default minecraft protocol version.
///
/// Just something to default to when real server version isn't known or when no hint is specified
/// in the configuration.
///
/// Should be kept up-to-date with latest supported Minecraft version by lazymc.
pub const PROTO_DEFAULT_PROTOCOL: u32 = 756;
/// Compression threshold to use.
// TODO: read this from server.properties instead
pub const COMPRESSION_THRESHOLD: i32 = 256;
/// Default buffer size when reading packets.
pub(super) const BUF_SIZE: usize = 8 * 1024;

View File

@@ -1,204 +1,25 @@
use std::io::prelude::*;
use std::sync::atomic::{AtomicI32, Ordering};
use std::sync::Mutex;
use bytes::BytesMut;
use flate2::read::ZlibDecoder;
use flate2::write::ZlibEncoder;
use flate2::Compression;
use minecraft_protocol::encoder::Encoder;
use minecraft_protocol::version::PacketId;
use tokio::io;
use tokio::io::AsyncReadExt;
use tokio::net::tcp::ReadHalf;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::tcp::{ReadHalf, WriteHalf};
use crate::proto::client::Client;
use crate::proto::BUF_SIZE;
use crate::types;
/// Default minecraft protocol version name.
///
/// Just something to default to when real server version isn't known or when no hint is specified
/// in the configuration.
///
/// Should be kept up-to-date with latest supported Minecraft version by lazymc.
pub const PROTO_DEFAULT_VERSION: &str = "1.17.1";
/// Default minecraft protocol version.
///
/// Just something to default to when real server version isn't known or when no hint is specified
/// in the configuration.
///
/// Should be kept up-to-date with latest supported Minecraft version by lazymc.
pub const PROTO_DEFAULT_PROTOCOL: u32 = 756;
/// Compression threshold to use.
// TODO: read this from server.properties instead
pub const COMPRESSION_THRESHOLD: i32 = 256;
/// Default buffer size when reading packets.
const BUF_SIZE: usize = 8 * 1024;
/// Minecraft protocol packet IDs.
#[allow(unused)]
pub mod packets {
pub mod handshake {
pub const SERVER_HANDSHAKE: i32 = 0;
}
pub mod status {
pub const CLIENT_STATUS: i32 = 0;
pub const CLIENT_PING: i32 = 1;
pub const SERVER_STATUS: i32 = 0;
pub const SERVER_PING: i32 = 1;
}
pub mod login {
pub const CLIENT_DISCONNECT: i32 = 0x00;
pub const CLIENT_LOGIN_SUCCESS: i32 = 0x02;
pub const CLIENT_SET_COMPRESSION: i32 = 0x03;
pub const SERVER_LOGIN_START: i32 = 0x00;
}
pub mod play {
pub const CLIENT_CHAT_MSG: i32 = 0x0F;
pub const CLIENT_PLUGIN_MESSAGE: i32 = 0x18;
pub const CLIENT_NAMED_SOUND_EFFECT: i32 = 0x19;
pub const CLIENT_DISCONNECT: i32 = 0x1A;
pub const CLIENT_KEEP_ALIVE: i32 = 0x21;
pub const CLIENT_JOIN_GAME: i32 = 0x26;
pub const CLIENT_PLAYER_POS_LOOK: i32 = 0x38;
pub const CLIENT_RESPAWN: i32 = 0x3D;
pub const CLIENT_SPAWN_POS: i32 = 0x4B;
pub const CLIENT_SET_TITLE_SUBTITLE: i32 = 0x57;
pub const CLIENT_TIME_UPDATE: i32 = 0x58;
pub const CLIENT_SET_TITLE_TEXT: i32 = 0x59;
pub const CLIENT_SET_TITLE_TIMES: i32 = 0x5A;
pub const SERVER_CLIENT_SETTINGS: i32 = 0x05;
pub const SERVER_PLUGIN_MESSAGE: i32 = 0x0A;
pub const SERVER_PLAYER_POS: i32 = 0x11;
pub const SERVER_PLAYER_POS_ROT: i32 = 0x12;
}
}
/// Client state.
///
/// Note: this does not keep track of encryption states.
#[derive(Debug)]
pub struct Client {
/// Current client state.
pub state: Mutex<ClientState>,
/// Compression state.
///
/// 0 or positive if enabled, negative if disabled.
pub compression: AtomicI32,
}
impl Client {
/// Get client state.
pub fn state(&self) -> ClientState {
*self.state.lock().unwrap()
}
/// Set client state.
pub fn set_state(&self, state: ClientState) {
*self.state.lock().unwrap() = state;
}
/// Get compression threshold.
pub fn compressed(&self) -> i32 {
self.compression.load(Ordering::Relaxed)
}
/// Whether compression is used.
pub fn is_compressed(&self) -> bool {
self.compressed() >= 0
}
/// Set compression value.
#[allow(unused)]
pub fn set_compression(&self, threshold: i32) {
trace!(target: "lazymc", "Client now uses compression threshold of {}", threshold);
self.compression.store(threshold, Ordering::Relaxed);
}
}
impl Default for Client {
fn default() -> Self {
Self {
state: Default::default(),
compression: AtomicI32::new(-1),
}
}
}
/// Protocol state a client may be in.
///
/// Note: this does not include the `play` state, because this is never used anymore when a client
/// reaches this state.
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub enum ClientState {
/// Initial client state.
Handshake,
/// State to query server status.
Status,
/// State to login to server.
Login,
/// State to play on the server.
#[allow(unused)]
Play,
}
impl ClientState {
/// From state ID.
pub fn from_id(id: i32) -> Option<Self> {
match id {
0 => Some(Self::Handshake),
1 => Some(Self::Status),
2 => Some(Self::Login),
_ => None,
}
}
/// Get state ID.
pub fn to_id(self) -> i32 {
match self {
Self::Handshake => 0,
Self::Status => 1,
Self::Login => 2,
Self::Play => -1,
}
}
}
impl Default for ClientState {
fn default() -> Self {
Self::Handshake
}
}
/// Client info, useful during connection handling.
#[derive(Debug, Default)]
pub struct ClientInfo {
/// Client protocol version.
pub protocol_version: Option<i32>,
/// Client username.
pub username: Option<String>,
}
impl ClientInfo {
pub fn empty() -> Self {
Self::default()
}
}
/// Raw Minecraft packet.
///
/// Having a packet ID and a raw data byte array.
pub struct RawPacket {
/// Packet ID.
pub id: i32,
pub id: u8,
/// Packet data.
pub data: Vec<u8>,
@@ -206,7 +27,7 @@ pub struct RawPacket {
impl RawPacket {
/// Construct new raw packet.
pub fn new(id: i32, data: Vec<u8>) -> Self {
pub fn new(id: u8, data: Vec<u8>) -> Self {
Self { id, data }
}
@@ -216,7 +37,7 @@ impl RawPacket {
let (read, packet_id) = types::read_var_int(buf)?;
buf = &buf[read..];
Ok(Self::new(packet_id, buf.to_vec()))
Ok(Self::new(packet_id as u8, buf.to_vec()))
}
/// Decode packet from raw buffer.
@@ -276,7 +97,7 @@ impl RawPacket {
/// Encode compressed packet to raw buffer.
fn encode_compressed(&self, threshold: i32) -> Result<Vec<u8>, ()> {
// Packet payload: packet ID and data buffer
let mut payload = types::encode_var_int(self.id)?;
let mut payload = types::encode_var_int(self.id as i32)?;
payload.extend_from_slice(&self.data);
// Determine whether to compress, encode data length bytes
@@ -307,7 +128,7 @@ impl RawPacket {
/// Encode uncompressed packet to raw buffer.
fn encode_uncompressed(&self) -> Result<Vec<u8>, ()> {
let mut data = types::encode_var_int(self.id)?;
let mut data = types::encode_var_int(self.id as i32)?;
data.extend_from_slice(&self.data);
let len = data.len() as i32;
@@ -378,3 +199,18 @@ pub async fn read_packet(
Ok(Some((packet, raw.to_vec())))
}
/// Write packet to stream writer.
pub async fn write_packet(
packet: impl PacketId + Encoder,
client: &Client,
writer: &mut WriteHalf<'_>,
) -> Result<(), ()> {
let mut data = Vec::new();
packet.encode(&mut data).map_err(|_| ())?;
let response = RawPacket::new(packet.packet_id(), data).encode(client)?;
writer.write_all(&response).await.map_err(|_| ())?;
Ok(())
}

41
src/proto/packets.rs Normal file
View File

@@ -0,0 +1,41 @@
//! Minecraft protocol packet IDs.
#![allow(unused)]
pub mod handshake {
pub const SERVER_HANDSHAKE: u8 = 0x00;
}
pub mod status {
pub const CLIENT_STATUS: u8 = 0x0;
pub const CLIENT_PING: u8 = 0x01;
pub const SERVER_STATUS: u8 = 0x00;
pub const SERVER_PING: u8 = 0x01;
}
pub mod login {
pub const CLIENT_DISCONNECT: u8 = 0x00;
pub const CLIENT_LOGIN_SUCCESS: u8 = 0x02;
pub const CLIENT_SET_COMPRESSION: u8 = 0x03;
pub const SERVER_LOGIN_START: u8 = 0x00;
}
pub mod play {
pub const CLIENT_CHAT_MSG: u8 = 0x0F;
pub const CLIENT_PLUGIN_MESSAGE: u8 = 0x18;
pub const CLIENT_NAMED_SOUND_EFFECT: u8 = 0x19;
pub const CLIENT_DISCONNECT: u8 = 0x1A;
pub const CLIENT_KEEP_ALIVE: u8 = 0x21;
pub const CLIENT_JOIN_GAME: u8 = 0x26;
pub const CLIENT_PLAYER_POS_LOOK: u8 = 0x38;
pub const CLIENT_RESPAWN: u8 = 0x3D;
pub const CLIENT_SPAWN_POS: u8 = 0x4B;
pub const CLIENT_SET_TITLE_SUBTITLE: u8 = 0x57;
pub const CLIENT_TIME_UPDATE: u8 = 0x58;
pub const CLIENT_SET_TITLE_TEXT: u8 = 0x59;
pub const CLIENT_SET_TITLE_TIMES: u8 = 0x5A;
pub const SERVER_CLIENT_SETTINGS: u8 = 0x05;
pub const SERVER_PLUGIN_MESSAGE: u8 = 0x0A;
pub const SERVER_PLAYER_POS: u8 = 0x11;
pub const SERVER_PLAYER_POS_ROT: u8 = 0x12;
}

View File

@@ -5,6 +5,8 @@ use tokio::io;
use tokio::io::AsyncWriteExt;
use tokio::net::TcpStream;
use crate::net;
/// Proxy the inbound stream to a target address.
pub async fn proxy(inbound: TcpStream, addr_target: SocketAddr) -> Result<(), Box<dyn Error>> {
proxy_with_queue(inbound, addr_target, &[]).await
@@ -64,5 +66,8 @@ pub async fn proxy_inbound_outbound_with_queue(
tokio::try_join!(client_to_server, server_to_client)?;
// Gracefully close connection if not done already
net::close_tcp_stream(inbound).await?;
Ok(())
}

View File

@@ -1,3 +1,4 @@
use std::net::IpAddr;
use std::sync::atomic::{AtomicU8, Ordering};
use std::sync::Arc;
use std::time::{Duration, Instant};
@@ -12,6 +13,7 @@ use tokio::sync::{Mutex, RwLock, RwLockReadGuard};
use tokio::time;
use crate::config::Config;
use crate::mc::ban::{BannedIp, BannedIps};
use crate::os;
/// Server cooldown after the process quit.
@@ -25,45 +27,6 @@ const SERVER_QUIT_COOLDOWN: Duration = Duration::from_millis(2500);
#[cfg(feature = "rcon")]
const RCON_COOLDOWN: Duration = Duration::from_secs(15);
/// Server state.
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub enum State {
/// Server is stopped.
Stopped,
/// Server is starting.
Starting,
/// Server is online and responding.
Started,
/// Server is stopping.
Stopping,
}
impl State {
/// From u8, panics if invalid.
pub fn from_u8(state: u8) -> Self {
match state {
0 => Self::Stopped,
1 => Self::Starting,
2 => Self::Started,
3 => Self::Stopping,
_ => panic!("invalid State u8"),
}
}
/// To u8.
pub fn to_u8(self) -> u8 {
match self {
Self::Stopped => 0,
Self::Starting => 1,
Self::Started => 2,
Self::Stopping => 3,
}
}
}
/// Shared server state.
#[derive(Debug)]
pub struct Server {
@@ -102,6 +65,9 @@ pub struct Server {
/// Used as starting/stopping timeout.
kill_at: RwLock<Option<Instant>>,
/// List of banned IPs.
banned_ips: RwLock<BannedIps>,
/// Lock for exclusive RCON operations.
#[cfg(feature = "rcon")]
rcon_lock: Semaphore,
@@ -345,6 +311,37 @@ impl Server {
.filter(|d| *d > 0)
.map(|d| Instant::now() + Duration::from_secs(d as u64));
}
/// Check whether the given IP is banned.
///
/// This uses the latest known `banned-ips.json` contents if known.
/// If this feature is disabled, this will always return false.
pub async fn is_banned_ip(&self, ip: &IpAddr) -> bool {
self.banned_ips.read().await.is_banned(ip)
}
/// Get user ban entry.
pub async fn ban_entry(&self, ip: &IpAddr) -> Option<BannedIp> {
self.banned_ips.read().await.get(ip)
}
/// Check whether the given IP is banned.
///
/// This uses the latest known `banned-ips.json` contents if known.
/// If this feature is disabled, this will always return false.
pub fn is_banned_ip_blocking(&self, ip: &IpAddr) -> bool {
futures::executor::block_on(async { self.is_banned_ip(ip).await })
}
/// Update the list of banned IPs.
pub async fn set_banned_ips(&self, ips: BannedIps) {
*self.banned_ips.write().await = ips;
}
/// Update the list of banned IPs.
pub fn set_banned_ips_blocking(&self, ips: BannedIps) {
futures::executor::block_on(async { self.set_banned_ips(ips).await })
}
}
impl Default for Server {
@@ -360,6 +357,7 @@ impl Default for Server {
last_active: Default::default(),
keep_online_until: Default::default(),
kill_at: Default::default(),
banned_ips: Default::default(),
#[cfg(feature = "rcon")]
rcon_lock: Semaphore::new(1),
#[cfg(feature = "rcon")]
@@ -368,6 +366,45 @@ impl Default for Server {
}
}
/// Server state.
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub enum State {
/// Server is stopped.
Stopped,
/// Server is starting.
Starting,
/// Server is online and responding.
Started,
/// Server is stopping.
Stopping,
}
impl State {
/// From u8, panics if invalid.
pub fn from_u8(state: u8) -> Self {
match state {
0 => Self::Stopped,
1 => Self::Starting,
2 => Self::Started,
3 => Self::Stopping,
_ => panic!("invalid State u8"),
}
}
/// To u8.
pub fn to_u8(self) -> u8 {
match self {
Self::Stopped => 0,
Self::Starting => 1,
Self::Started => 2,
Self::Stopping => 3,
}
}
}
/// Invoke server command, store PID and wait for it to quit.
pub async fn invoke_server_cmd(
config: Arc<Config>,

119
src/service/ban_reload.rs Normal file
View File

@@ -0,0 +1,119 @@
use std::path::Path;
use std::sync::mpsc::channel;
use std::sync::Arc;
use std::thread;
use std::time::Duration;
use notify::{watcher, DebouncedEvent, RecursiveMode, Watcher};
use crate::config::Config;
use crate::mc::ban;
use crate::server::Server;
/// File debounce time.
const WATCH_DEBOUNCE: Duration = Duration::from_secs(2);
/// Service to reload banned IPs when its file changes.
pub fn service(config: Arc<Config>, server: Arc<Server>) {
// TODO: check what happens when file doesn't exist at first?
// Ensure we need to reload banned IPs
if !config.server.block_banned_ips && !config.server.drop_banned_ips {
return;
}
// Ensure server directory is set, it must exist
let dir = match &config.server.directory {
Some(dir) => dir,
None => {
warn!(target: "lazymc", "Not blocking banned IPs, server directory not configured, unable to find {} file", ban::FILE);
return;
}
};
// Determine file path, ensure it exists
let path = dir.join(crate::mc::ban::FILE);
if !path.is_file() {
warn!(target: "lazymc", "Not blocking banned IPs, {} file does not exist", ban::FILE);
return;
}
// Load banned IPs once
match ban::load(&path) {
Ok(ips) => server.set_banned_ips_blocking(ips),
Err(err) => {
error!(target: "lazymc", "Failed to load banned IPs from {}: {}", ban::FILE, err);
}
}
// Show warning if 127.0.0.1 is banned
if server.is_banned_ip_blocking(&("127.0.0.1".parse().unwrap())) {
warn!(target: "lazymc", "Local address 127.0.0.1 IP banned, probably not what you want");
warn!(target: "lazymc", "Use '/pardon-ip 127.0.0.1' on the server to unban");
}
// Keep watching
while watch(&server, &path) {}
}
/// Watch the given file.
fn watch(server: &Server, path: &Path) -> bool {
// The file must exist
if !path.is_file() {
warn!(target: "lazymc", "File {} does not exist, not watching changes", ban::FILE);
return false;
}
// Create watcher for banned IPs file
let (tx, rx) = channel();
let mut watcher =
watcher(tx, WATCH_DEBOUNCE).expect("failed to create watcher for banned-ips.json");
if let Err(err) = watcher.watch(path, RecursiveMode::NonRecursive) {
error!(target: "lazymc", "An error occured while creating watcher for {}: {}", ban::FILE, err);
return true;
}
loop {
// Take next event
let event = rx.recv().unwrap();
// Decide whether to reload and rewatch
let (reload, rewatch) = match event {
// Reload on write
DebouncedEvent::NoticeWrite(_) | DebouncedEvent::Write(_) => (true, false),
// Reload and rewatch on rename/remove
DebouncedEvent::NoticeRemove(_)
| DebouncedEvent::Remove(_)
| DebouncedEvent::Rename(_, _)
| DebouncedEvent::Rescan
| DebouncedEvent::Create(_) => {
trace!(target: "lazymc", "File banned-ips.json removed, trying to rewatch after 1 second");
thread::sleep(WATCH_DEBOUNCE);
(true, true)
}
// Ignore chmod changes
DebouncedEvent::Chmod(_) => (false, false),
// Rewatch on error
DebouncedEvent::Error(_, _) => (false, true),
};
// Reload banned IPs
if reload {
info!(target: "lazymc", "Reloading list of banned IPs...");
match ban::load(path) {
Ok(ips) => server.set_banned_ips_blocking(ips),
Err(err) => {
error!(target: "lazymc", "Failed reload list of banned IPs from {}: {}", ban::FILE, err);
}
}
}
// Rewatch
if rewatch {
return true;
}
}
}

View File

@@ -1,3 +1,4 @@
pub mod ban_reload;
pub mod monitor;
pub mod server;
pub mod signal;

View File

@@ -6,7 +6,7 @@ use futures::FutureExt;
use tokio::net::{TcpListener, TcpStream};
use crate::config::Config;
use crate::proto::Client;
use crate::proto::client::Client;
use crate::proxy;
use crate::server::{self, Server};
use crate::service;
@@ -49,6 +49,10 @@ pub async fn service(config: Arc<Config>) -> Result<(), ()> {
// Spawn server monitor and signal handler services
tokio::spawn(service::monitor::service(config.clone(), server.clone()));
tokio::spawn(service::signal::service(config.clone(), server.clone()));
tokio::task::spawn_blocking({
let (config, server) = (config.clone(), server.clone());
|| service::ban_reload::service(config, server)
});
// Initiate server start
if config.server.wake_on_start {
@@ -66,19 +70,37 @@ pub async fn service(config: Arc<Config>) -> Result<(), ()> {
/// Route inbound TCP stream to correct service, spawning a new task.
#[inline]
fn route(inbound: TcpStream, config: Arc<Config>, server: Arc<Server>) {
let should_proxy = server.state() == server::State::Started && !config.lockout.enabled;
// Get user peer address
let peer = match inbound.peer_addr() {
Ok(peer) => peer,
Err(err) => {
warn!(target: "lazymc", "Connection from unknown peer address, disconnecting: {}", err);
return;
}
};
// Check ban state, just drop connection if enabled
let banned = server.is_banned_ip_blocking(&peer.ip());
if config.server.drop_banned_ips {
info!(target: "lazymc", "Connection from banned IP {}, dropping", peer.ip());
return;
}
// Route connection through proper channel
let should_proxy =
!banned && server.state() == server::State::Started && !config.lockout.enabled;
if should_proxy {
route_proxy(inbound, config)
} else {
route_status(inbound, config, server)
route_status(inbound, config, server, peer)
}
}
/// Route inbound TCP stream to status server, spawning a new task.
#[inline]
fn route_status(inbound: TcpStream, config: Arc<Config>, server: Arc<Server>) {
fn route_status(inbound: TcpStream, config: Arc<Config>, server: Arc<Server>, peer: SocketAddr) {
// When server is not online, spawn a status server
let client = Client::default();
let client = Client::new(peer);
let service = status::serve(client, inbound, config, server).map(|r| {
if let Err(err) = r {
warn!(target: "lazymc", "Failed to serve status: {:?}", err);

View File

@@ -1,6 +1,4 @@
use std::ops::Deref;
use std::sync::Arc;
use std::time::Duration;
use bytes::BytesMut;
use minecraft_protocol::data::chat::{Message, Payload};
@@ -8,19 +6,28 @@ use minecraft_protocol::data::server_status::*;
use minecraft_protocol::decoder::Decoder;
use minecraft_protocol::encoder::Encoder;
use minecraft_protocol::version::v1_14_4::handshake::Handshake;
use minecraft_protocol::version::v1_14_4::login::{LoginDisconnect, LoginStart};
use minecraft_protocol::version::v1_14_4::login::LoginStart;
use minecraft_protocol::version::v1_14_4::status::StatusResponse;
use tokio::io::{self, AsyncWriteExt};
use tokio::net::tcp::WriteHalf;
use tokio::fs;
use tokio::io::AsyncWriteExt;
use tokio::net::TcpStream;
use tokio::time;
use crate::config::*;
#[cfg(feature = "lobby")]
use crate::lobby;
use crate::proto::{self, Client, ClientInfo, ClientState, RawPacket};
use crate::server::{self, Server, State};
use crate::service;
use crate::join;
use crate::proto::action;
use crate::proto::client::{Client, ClientInfo, ClientState};
use crate::proto::packet::{self, RawPacket};
use crate::proto::packets;
use crate::server::{self, Server};
/// The ban message prefix.
const BAN_MESSAGE_PREFIX: &str = "Your IP address is banned from this server.\nReason: ";
/// Default ban reason if unknown.
const DEFAULT_BAN_REASON: &str = "Banned by an operator.";
/// Server icon file path.
const SERVER_ICON_FILE: &str = "server-icon.png";
/// Proxy the given inbound stream to a target address.
// TODO: do not drop error here, return Box<dyn Error>
@@ -35,16 +42,13 @@ pub async fn serve(
// Incoming buffer and packet holding queue
let mut buf = BytesMut::new();
// Remember inbound packets, used for client holding and forwarding
let remember_inbound = config.join.methods.contains(&Method::Hold)
|| config.join.methods.contains(&Method::Forward);
// Remember inbound packets, track client info
let mut inbound_history = BytesMut::new();
let mut client_info = ClientInfo::empty();
loop {
// Read packet from stream
let (packet, raw) = match proto::read_packet(&client, &mut buf, &mut reader).await {
let (packet, raw) = match packet::read_packet(&client, &mut buf, &mut reader).await {
Ok(Some(packet)) => packet,
Ok(None) => break,
Err(_) => {
@@ -58,7 +62,7 @@ pub async fn serve(
// Hijack handshake
if client_state == ClientState::Handshake
&& packet.id == proto::packets::handshake::SERVER_HANDSHAKE
&& packet.id == packets::handshake::SERVER_HANDSHAKE
{
// Parse handshake
let handshake = match Handshake::decode(&mut packet.data.as_slice()) {
@@ -84,8 +88,8 @@ pub async fn serve(
.replace(handshake.protocol_version);
client.set_state(new_state);
// If login handshake and holding is enabled, hold packets
if new_state == ClientState::Login && remember_inbound {
// If loggin in with handshake, remember inbound
if new_state == ClientState::Login {
inbound_history.extend(raw);
}
@@ -93,8 +97,7 @@ pub async fn serve(
}
// Hijack server status packet
if client_state == ClientState::Status && packet.id == proto::packets::status::SERVER_STATUS
{
if client_state == ClientState::Status && packet.id == packets::status::SERVER_STATUS {
let server_status = server_status(&config, &server).await;
let packet = StatusResponse { server_status };
@@ -108,15 +111,13 @@ pub async fn serve(
}
// Hijack ping packet
if client_state == ClientState::Status && packet.id == proto::packets::status::SERVER_PING {
if client_state == ClientState::Status && packet.id == packets::status::SERVER_PING {
writer.write_all(&raw).await.map_err(|_| ())?;
continue;
}
// Hijack login start
if client_state == ClientState::Login
&& packet.id == proto::packets::login::SERVER_LOGIN_START
{
if client_state == ClientState::Login && packet.id == packets::login::SERVER_LOGIN_START {
// Try to get login username, update client info
// TODO: we should always parse this packet successfully
let username = LoginStart::decode(&mut packet.data.as_slice())
@@ -132,100 +133,57 @@ pub async fn serve(
}
None => info!(target: "lazymc", "Kicked player because lockout is enabled"),
}
kick(&client, &config.lockout.message, &mut writer).await?;
action::kick(&client, &config.lockout.message, &mut writer).await?;
break;
}
// Kick if client is banned
if let Some(ban) = server.ban_entry(&client.peer.ip()).await {
if ban.is_banned() {
let msg = if let Some(reason) = ban.reason {
info!(target: "lazymc", "Login from banned IP {} ({}), disconnecting", client.peer.ip(), &reason);
reason.to_string()
} else {
info!(target: "lazymc", "Login from banned IP {}, disconnecting", client.peer.ip());
DEFAULT_BAN_REASON.to_string()
};
action::kick(
&client,
&format!("{}{}", BAN_MESSAGE_PREFIX, msg),
&mut writer,
)
.await?;
break;
}
}
// Start server if not starting yet
Server::start(config.clone(), server.clone(), username).await;
// Use join occupy methods
for method in &config.join.methods {
match method {
// Kick method, immediately kick client
Method::Kick => {
trace!(target: "lazymc", "Using kick method to occupy joining client");
// Select message and kick
let msg = match server.state() {
server::State::Starting
| server::State::Stopped
| server::State::Started => &config.join.kick.starting,
server::State::Stopping => &config.join.kick.stopping,
};
kick(&client, msg, &mut writer).await?;
break;
}
// Hold method, hold client connection while server starts
Method::Hold => {
trace!(target: "lazymc", "Using hold method to occupy joining client");
// Server must be starting
if server.state() != State::Starting {
continue;
}
// Hold login packet and remaining read bytes
// Remember inbound packets
inbound_history.extend(&raw);
inbound_history.extend(buf.split_off(0));
inbound_history.extend(&buf);
// Start holding
if hold(&config, &server).await? {
service::server::route_proxy_queue(inbound, config, inbound_history);
return Ok(());
}
}
// Build inbound packet queue with everything from login start (including this)
let mut login_queue = BytesMut::with_capacity(raw.len() + buf.len());
login_queue.extend(&raw);
login_queue.extend(&buf);
// Forward method, forward client connection while server starts
Method::Forward => {
trace!(target: "lazymc", "Using forward method to occupy joining client");
// Buf is fully consumed here
buf.clear();
// Hold login packet and remaining read bytes
inbound_history.extend(&raw);
inbound_history.extend(buf.split_off(0));
// Forward client
debug!(target: "lazymc", "Forwarding client to {:?}!", config.join.forward.address);
service::server::route_proxy_address_queue(
// Start occupying client
join::occupy(
client,
client_info,
config,
server,
inbound,
config.join.forward.address,
inbound_history,
);
login_queue,
)
.await?;
return Ok(());
// TODO: do not consume client here, allow other join method on fail
}
// Lobby method, keep client in lobby while server starts
#[cfg(feature = "lobby")]
Method::Lobby => {
trace!(target: "lazymc", "Using lobby method to occupy joining client");
// Build queue with login packet and any additionally received
let mut queue = BytesMut::with_capacity(raw.len() + buf.len());
queue.extend(raw);
queue.extend(buf.split_off(0));
// Start lobby
lobby::serve(client, client_info, inbound, config, server, queue).await?;
return Ok(());
// TODO: do not consume client here, allow other join method on fail
}
// Lobby method, keep client in lobby while server starts
#[cfg(not(feature = "lobby"))]
Method::Lobby => {
error!(target: "lazymc", "Lobby join method not supported in this lazymc build");
}
}
}
debug!(target: "lazymc", "No method left to occupy joining client, disconnecting");
// Done occupying client, just disconnect
break;
}
// Show unhandled packet warning
@@ -234,99 +192,18 @@ pub async fn serve(
debug!(target: "lazymc", "- Packet ID: {}", packet.id);
}
// Gracefully close connection
match writer.shutdown().await {
Ok(_) => {}
Err(err) if err.kind() == io::ErrorKind::NotConnected => {}
Err(_) => return Err(()),
}
Ok(())
}
/// Hold a client while server starts.
///
/// Returns holding status. `true` if client is held and it should be proxied, `false` it was held
/// but it timed out.
pub async fn hold<'a>(config: &Config, server: &Server) -> Result<bool, ()> {
trace!(target: "lazymc", "Started holding client");
// A task to wait for suitable server state
// Waits for started state, errors if stopping/stopped state is reached
let task_wait = async {
let mut state = server.state_receiver();
loop {
// Wait for state change
state.changed().await.unwrap();
match state.borrow().deref() {
// Still waiting on server start
State::Starting => {
trace!(target: "lazymc", "Server not ready, holding client for longer");
continue;
}
// Server started, start relaying and proxy
State::Started => {
break true;
}
// Server stopping, this shouldn't happen, kick
State::Stopping => {
warn!(target: "lazymc", "Server stopping for held client, disconnecting");
break false;
}
// Server stopped, this shouldn't happen, disconnect
State::Stopped => {
error!(target: "lazymc", "Server stopped for held client, disconnecting");
break false;
}
}
}
};
// Wait for server state with timeout
let timeout = Duration::from_secs(config.join.hold.timeout as u64);
match time::timeout(timeout, task_wait).await {
// Relay client to proxy
Ok(true) => {
info!(target: "lazymc", "Server ready for held client, relaying to server");
Ok(true)
}
// Server stopping/stopped, this shouldn't happen, kick
Ok(false) => {
warn!(target: "lazymc", "Server stopping for held client");
Ok(false)
}
// Timeout reached, kick with starting message
Err(_) => {
warn!(target: "lazymc", "Held client reached timeout of {}s", config.join.hold.timeout);
Ok(false)
}
}
}
/// Kick client with a message.
///
/// Should close connection afterwards.
async fn kick(client: &Client, msg: &str, writer: &mut WriteHalf<'_>) -> Result<(), ()> {
let packet = LoginDisconnect {
reason: Message::new(Payload::text(msg)),
};
let mut data = Vec::new();
packet.encode(&mut data).map_err(|_| ())?;
let response = RawPacket::new(proto::packets::login::CLIENT_DISCONNECT, data).encode(client)?;
writer.write_all(&response).await.map_err(|_| ())
}
/// Build server status object to respond to client with.
async fn server_status(config: &Config, server: &Server) -> ServerStatus {
let status = server.status().await;
let server_state = server.state();
// Respond with real server status if started
if server_state == server::State::Started && status.is_some() {
return status.as_ref().unwrap().clone();
}
// Select version and player max from last known server status
let (version, max) = match status.as_ref() {
@@ -345,7 +222,7 @@ async fn server_status(config: &Config, server: &Server) -> ServerStatus {
if config.motd.from_server && status.is_some() {
status.as_ref().unwrap().description.clone()
} else {
Message::new(Payload::text(match server.state() {
Message::new(Payload::text(match server_state {
server::State::Stopped | server::State::Started => &config.motd.sleeping,
server::State::Starting => &config.motd.starting,
server::State::Stopping => &config.motd.stopping,
@@ -353,6 +230,13 @@ async fn server_status(config: &Config, server: &Server) -> ServerStatus {
}
};
// Get server favicon
let favicon = if config.motd.from_server && status.is_some() {
status.as_ref().unwrap().favicon.clone()
} else {
favicon(&config).await
};
// Build status resposne
ServerStatus {
version,
@@ -362,5 +246,36 @@ async fn server_status(config: &Config, server: &Server) -> ServerStatus {
max,
sample: vec![],
},
favicon,
}
}
/// Get server status favicon.
async fn favicon(config: &Config) -> Option<String> {
// Get server dir
let dir = match config.server.directory.as_ref() {
Some(dir) => dir,
None => return None,
};
// Server icon file, ensure it exists
let path = dir.join(SERVER_ICON_FILE);
if !path.is_file() {
return None;
}
// Read icon data
let data = fs::read(path)
.await
.map_err(|err| {
error!(target: "lazymc", "Failed to read favicon from {}: {}", SERVER_ICON_FILE, err);
})
.ok()?;
// Format and return favicon
Some(format!(
"{}{}",
"data:image/png;base64,",
base64::encode(data)
))
}

View File

@@ -1,5 +1,6 @@
pub mod cli;
pub mod error;
pub mod serde;
pub mod style;
use std::env;

30
src/util/serde.rs Normal file
View File

@@ -0,0 +1,30 @@
use std::net::{SocketAddr, ToSocketAddrs};
use serde::de::{Error, Unexpected};
use serde::{Deserialize, Deserializer};
/// Deserialize a `Vec` into a `HashMap` by key.
pub fn to_socket_addrs<'de, D>(d: D) -> Result<SocketAddr, D::Error>
where
D: Deserializer<'de>,
{
// Deserialize string
let addr = String::deserialize(d)?;
// Try to socket address to resolve
match addr.to_socket_addrs() {
Ok(mut addr) => {
if let Some(addr) = addr.next() {
return Ok(addr);
}
}
Err(err) => {
dbg!(err);
}
}
// Parse raw IP address
addr.parse().map_err(|_| {
Error::invalid_value(Unexpected::Str(&addr), &"IP or resolvable host and port")
})
}