Compare commits
174 Commits
Author | SHA1 | Date | |
---|---|---|---|
c3d1618ba8 | |||
|
37fdb9c12a | ||
|
e7a3db19aa | ||
|
ca4753673d | ||
|
0124aa723d | ||
|
afbc54758c | ||
|
e54025f02f | ||
|
023e46fe64 | ||
|
6622962d5d | ||
|
da60287e10 | ||
|
eb5ee7defd | ||
|
fdeb7594c2 | ||
|
5c7e17b0ae | ||
|
be74e053f4 | ||
|
eb2cf1219e | ||
|
72d58f051f | ||
|
6c3129a8b0 | ||
|
cc2061ad7d | ||
|
342a55471c | ||
|
982a604d34 | ||
|
6b463ac8c8 | ||
|
c7caebe6a8 | ||
|
aa1a74682e | ||
|
835ca62c06 | ||
|
b609f86bde | ||
|
4b1857f48d | ||
|
bc7bd908f6 | ||
|
a3fef88eac | ||
|
57117b29f3 | ||
|
5ef7c54ec6 | ||
|
5f13132c57 | ||
|
226215479c | ||
|
f4870c66fa | ||
|
c415420eae | ||
|
c6db4d7c3f | ||
|
39feb0bdc2 | ||
|
35fff7168b | ||
|
e5e5947a16 | ||
|
2e6551b009 | ||
|
2c00dba5e8 | ||
|
0e4d18e9f6 | ||
|
026aa58b5d | ||
|
0ac9a07c93 | ||
|
540137b93e | ||
|
05dfd19d80 | ||
|
f02217abd3 | ||
|
3185ca855c | ||
|
2c43446ed0 | ||
|
f7fe00aa50 | ||
|
f2087792b4 | ||
|
d46f8375c7 | ||
|
2d8173aba8 | ||
|
bd9f81f1f0 | ||
|
b561351a2a | ||
|
d3cb880dd0 | ||
|
716cd48eac | ||
|
c6f860f013 | ||
|
8d4ace60a6 | ||
|
09c6d1d996 | ||
|
dca2f8eb5c | ||
|
1f6665a90f | ||
|
0c77a18d96 | ||
|
0c69752fec | ||
|
6e8ff5b1b3 | ||
|
fba581d4bd | ||
|
d7b601d6e3 | ||
|
4d76058472 | ||
|
5b5a2bf8ae | ||
|
c477e45553 | ||
|
69de7a95bf | ||
|
3e7e5c4c03 | ||
|
4d774cd254 | ||
|
a1324d22a7 | ||
|
a500f50064 | ||
|
4ef1481f2b | ||
|
addfb1c135 | ||
|
956c428251 | ||
|
270362b152 | ||
|
93f75adc5c | ||
|
40fe5b5dd2 | ||
|
e7d2c6f64c | ||
|
084c3c5f8b | ||
|
d39ad9c913 | ||
|
00b6cddd78 | ||
|
b8744aaf57 | ||
|
51d3ecf148 | ||
|
75c7a09249 | ||
|
f6d60318e8 | ||
|
b404ab0a87 | ||
|
cf6bd526d9 | ||
|
f513957bff | ||
|
9b1f2a7011 | ||
|
8b09faae3d | ||
|
e23a61ab0f | ||
|
78a36978f5 | ||
|
0ba46caf5c | ||
|
6b23490919 | ||
|
9e08ed6cda | ||
|
3271db1cb3 | ||
|
cf0e3ef15b | ||
|
ee21eb45fd | ||
|
aebb5563e0 | ||
|
20fb6ee715 | ||
|
ea2dbc905c | ||
|
eb66265670 | ||
|
df101ce53b | ||
|
8f2ce9b4b8 | ||
|
20902e6a94 | ||
|
3e933f7566 | ||
|
46fa594065 | ||
|
1d92964802 | ||
|
9b8d569628 | ||
|
94f2fa01e2 | ||
|
9b71052b61 | ||
|
0049ad456c | ||
|
0f2d7720af | ||
|
723ebabcfb | ||
|
f95682fcd5 | ||
|
d5c854d16f | ||
|
493e24ff4d | ||
|
6916800aeb | ||
|
e7c31f2619 | ||
|
7da467ff8c | ||
|
c9d7af0e3c | ||
|
0715baed8c | ||
|
1f4ec11ad1 | ||
|
acf6768b49 | ||
|
75f7b62b16 | ||
|
9cc1958bbd | ||
|
785bd2f33e | ||
|
b168dcefde | ||
|
74d772ab42 | ||
|
a71b3cb24f | ||
|
28dbcdbfd6 | ||
|
e816d4ff6c | ||
|
168cbceb4c | ||
|
b1bd9e1837 | ||
|
ec24f088b2 | ||
|
6321999489 | ||
|
47fe7d0387 | ||
|
7df3829e00 | ||
|
b06f26b3e8 | ||
|
4510586169 | ||
|
73d0c86780 | ||
|
aca09dff7b | ||
|
de516cf62c | ||
|
1da8c60323 | ||
|
d213612225 | ||
|
ffc4f00430 | ||
|
90e64297c0 | ||
|
ae6e877f17 | ||
|
4907780f7c | ||
|
40be4bae89 | ||
|
d390f866cd | ||
|
374a9fab75 | ||
|
f7d89a28aa | ||
|
8b88cb16c5 | ||
|
2cc64b29e0 | ||
|
802fd2990a | ||
|
3e7f5719cd | ||
|
32317a4c2f | ||
|
c9290827be | ||
|
518fca90eb | ||
|
e01fd212f7 | ||
|
db99289ea7 | ||
|
32cd9ffc73 | ||
|
d125140bee | ||
|
e11eca1d5a | ||
|
17ec663e15 | ||
|
6b38dce5ab | ||
|
234a30aecb | ||
|
7a99781a05 | ||
|
5a5fa785b7 | ||
|
78e9abec59 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -4,3 +4,4 @@
|
|||||||
|
|
||||||
# Test server
|
# Test server
|
||||||
/mcserver
|
/mcserver
|
||||||
|
/bettermc
|
||||||
|
139
.gitlab-ci.yml
139
.gitlab-ci.yml
@@ -9,18 +9,16 @@ stages:
|
|||||||
|
|
||||||
# Variable defaults
|
# Variable defaults
|
||||||
variables:
|
variables:
|
||||||
|
RUST_VERSION: stable
|
||||||
TARGET: x86_64-unknown-linux-gnu
|
TARGET: x86_64-unknown-linux-gnu
|
||||||
|
|
||||||
# Rust build cache configuration
|
|
||||||
.rust-build-cache: &rust-build-cache
|
|
||||||
key: "$CI_PIPELINE_ID"
|
|
||||||
paths:
|
|
||||||
- target/
|
|
||||||
|
|
||||||
# Install build dependencies
|
# Install build dependencies
|
||||||
before_script:
|
before_script:
|
||||||
- apt-get update
|
- apt-get update
|
||||||
- apt-get install -y --no-install-recommends build-essential
|
- apt-get install -y --no-install-recommends build-essential
|
||||||
|
- |
|
||||||
|
rustup install $RUST_VERSION
|
||||||
|
rustup default $RUST_VERSION
|
||||||
- |
|
- |
|
||||||
rustc --version
|
rustc --version
|
||||||
cargo --version
|
cargo --version
|
||||||
@@ -29,51 +27,39 @@ before_script:
|
|||||||
.before_script-windows: &before_script-windows
|
.before_script-windows: &before_script-windows
|
||||||
before_script:
|
before_script:
|
||||||
# Install scoop
|
# Install scoop
|
||||||
- Invoke-Expression (New-Object System.Net.WebClient).DownloadString('https://get.scoop.sh')
|
- iex "& {$(irm get.scoop.sh)} -RunAsAdmin"
|
||||||
|
|
||||||
# Install Rust
|
# Install Rust
|
||||||
- scoop install rustup
|
- scoop install rustup gcc
|
||||||
|
- rustup install $RUST_VERSION
|
||||||
|
- rustup default $RUST_VERSION
|
||||||
- rustc --version
|
- rustc --version
|
||||||
- cargo --version
|
- cargo --version
|
||||||
|
|
||||||
|
# Install proper Rust target
|
||||||
|
- rustup target install x86_64-pc-windows-msvc
|
||||||
|
|
||||||
# Check on stable, beta and nightly
|
# Check on stable, beta and nightly
|
||||||
.check-base: &check-base
|
.check-base: &check-base
|
||||||
stage: check
|
stage: check
|
||||||
cache:
|
|
||||||
<<: *rust-build-cache
|
|
||||||
script:
|
script:
|
||||||
- cargo check --verbose
|
- cargo check --verbose
|
||||||
|
- cargo check --no-default-features --verbose
|
||||||
- cargo check --no-default-features --features rcon --verbose
|
- cargo check --no-default-features --features rcon --verbose
|
||||||
check:
|
- cargo check --no-default-features --features lobby --verbose
|
||||||
|
check-stable:
|
||||||
<<: *check-base
|
<<: *check-base
|
||||||
check-macos:
|
check-msrv:
|
||||||
tags:
|
<<: *check-base
|
||||||
- macos
|
variables:
|
||||||
|
RUST_VERSION: 1.64.0
|
||||||
only:
|
only:
|
||||||
- master
|
- master
|
||||||
- /^v(\d+\.)*\d+$/
|
|
||||||
before_script:
|
|
||||||
- rustup default stable
|
|
||||||
- |
|
|
||||||
rustc --version
|
|
||||||
cargo --version
|
|
||||||
<<: *check-base
|
|
||||||
check-windows:
|
|
||||||
stage: check
|
|
||||||
tags:
|
|
||||||
- windows
|
|
||||||
cache: {}
|
|
||||||
<<: *before_script-windows
|
|
||||||
script:
|
|
||||||
- cargo check --locked --verbose
|
|
||||||
- cargo check --locked --no-default-features --features rcon --verbose
|
|
||||||
|
|
||||||
# Build using Rust stable on Linux
|
# Build using Rust stable on Linux
|
||||||
build-x86_64-linux-gnu:
|
build-x86_64-linux-gnu:
|
||||||
stage: build
|
stage: build
|
||||||
needs: []
|
needs: []
|
||||||
cache:
|
|
||||||
<<: *rust-build-cache
|
|
||||||
script:
|
script:
|
||||||
- cargo build --target=$TARGET --release --locked --verbose
|
- cargo build --target=$TARGET --release --locked --verbose
|
||||||
- mv target/$TARGET/release/lazymc ./lazymc-$TARGET
|
- mv target/$TARGET/release/lazymc ./lazymc-$TARGET
|
||||||
@@ -87,11 +73,12 @@ build-x86_64-linux-gnu:
|
|||||||
# Build a static version
|
# Build a static version
|
||||||
build-x86_64-linux-musl:
|
build-x86_64-linux-musl:
|
||||||
stage: build
|
stage: build
|
||||||
|
only:
|
||||||
|
- master
|
||||||
|
- /^v(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/
|
||||||
needs: []
|
needs: []
|
||||||
variables:
|
variables:
|
||||||
TARGET: x86_64-unknown-linux-musl
|
TARGET: x86_64-unknown-linux-musl
|
||||||
cache:
|
|
||||||
<<: *rust-build-cache
|
|
||||||
script:
|
script:
|
||||||
- rustup target add $TARGET
|
- rustup target add $TARGET
|
||||||
- cargo build --target=$TARGET --release --locked --verbose
|
- cargo build --target=$TARGET --release --locked --verbose
|
||||||
@@ -110,11 +97,12 @@ build-x86_64-linux-musl:
|
|||||||
build-armv7-linux-gnu:
|
build-armv7-linux-gnu:
|
||||||
stage: build
|
stage: build
|
||||||
image: ubuntu
|
image: ubuntu
|
||||||
|
only:
|
||||||
|
- master
|
||||||
|
- /^v(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/
|
||||||
needs: []
|
needs: []
|
||||||
variables:
|
variables:
|
||||||
TARGET: armv7-unknown-linux-gnueabihf
|
TARGET: armv7-unknown-linux-gnueabihf
|
||||||
cache:
|
|
||||||
<<: *rust-build-cache
|
|
||||||
before_script:
|
before_script:
|
||||||
- apt-get update
|
- apt-get update
|
||||||
- apt-get install -y --no-install-recommends build-essential
|
- apt-get install -y --no-install-recommends build-essential
|
||||||
@@ -145,11 +133,12 @@ build-armv7-linux-gnu:
|
|||||||
build-aarch64-linux-gnu:
|
build-aarch64-linux-gnu:
|
||||||
stage: build
|
stage: build
|
||||||
image: ubuntu
|
image: ubuntu
|
||||||
|
only:
|
||||||
|
- master
|
||||||
|
- /^v(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/
|
||||||
needs: []
|
needs: []
|
||||||
variables:
|
variables:
|
||||||
TARGET: aarch64-unknown-linux-gnu
|
TARGET: aarch64-unknown-linux-gnu
|
||||||
cache:
|
|
||||||
<<: *rust-build-cache
|
|
||||||
before_script:
|
before_script:
|
||||||
- apt-get update
|
- apt-get update
|
||||||
- apt-get install -y --no-install-recommends build-essential
|
- apt-get install -y --no-install-recommends build-essential
|
||||||
@@ -176,36 +165,14 @@ build-aarch64-linux-gnu:
|
|||||||
- lazymc-$TARGET
|
- lazymc-$TARGET
|
||||||
expire_in: 1 month
|
expire_in: 1 month
|
||||||
|
|
||||||
# Build using Rust stable on macOS
|
|
||||||
build-macos:
|
|
||||||
stage: build
|
|
||||||
tags:
|
|
||||||
- macos
|
|
||||||
only:
|
|
||||||
- master
|
|
||||||
- /^v(\d+\.)*\d+$/
|
|
||||||
needs: []
|
|
||||||
variables:
|
|
||||||
TARGET: x86_64-apple-darwin
|
|
||||||
before_script:
|
|
||||||
- rustup default stable
|
|
||||||
- |
|
|
||||||
rustc --version
|
|
||||||
cargo --version
|
|
||||||
script:
|
|
||||||
- cargo build --target=$TARGET --release --locked --verbose
|
|
||||||
- mv target/$TARGET/release/lazymc ./lazymc-$TARGET
|
|
||||||
artifacts:
|
|
||||||
name: lazymc-x86_64-macos
|
|
||||||
paths:
|
|
||||||
- lazymc-$TARGET
|
|
||||||
expire_in: 1 month
|
|
||||||
|
|
||||||
# Build using Rust stable on Windows
|
# Build using Rust stable on Windows
|
||||||
build-x86_64-windows:
|
build-x86_64-windows:
|
||||||
stage: build
|
stage: build
|
||||||
tags:
|
tags:
|
||||||
- windows
|
- windows
|
||||||
|
only:
|
||||||
|
- master
|
||||||
|
- /^v(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/
|
||||||
needs: []
|
needs: []
|
||||||
variables:
|
variables:
|
||||||
TARGET: x86_64-pc-windows-msvc
|
TARGET: x86_64-pc-windows-msvc
|
||||||
@@ -222,24 +189,28 @@ build-x86_64-windows:
|
|||||||
# Run the unit tests through Cargo on Linux
|
# Run the unit tests through Cargo on Linux
|
||||||
test-cargo-x86_64-linux-gnu:
|
test-cargo-x86_64-linux-gnu:
|
||||||
stage: test
|
stage: test
|
||||||
|
only:
|
||||||
|
- master
|
||||||
needs: []
|
needs: []
|
||||||
dependencies: []
|
dependencies: []
|
||||||
cache:
|
|
||||||
<<: *rust-build-cache
|
|
||||||
script:
|
script:
|
||||||
- cargo test --locked --verbose
|
- cargo test --locked --verbose
|
||||||
|
- cargo test --locked --no-default-features --verbose
|
||||||
|
- cargo test --locked --no-default-features --features rcon --verbose
|
||||||
|
- cargo test --locked --no-default-features --features lobby --verbose
|
||||||
|
|
||||||
# Run the unit tests through Cargo on Windows
|
# # Run the unit tests through Cargo on Windows
|
||||||
test-cargo-x86_64-windows:
|
# test-cargo-x86_64-windows:
|
||||||
stage: test
|
# stage: test
|
||||||
tags:
|
# tags:
|
||||||
- windows
|
# - windows
|
||||||
needs: []
|
# needs: []
|
||||||
dependencies: []
|
# dependencies: []
|
||||||
cache: {}
|
# <<: *before_script-windows
|
||||||
<<: *before_script-windows
|
# script:
|
||||||
script:
|
# - cargo test --locked --verbose
|
||||||
- cargo test --locked --verbose
|
# - cargo test --locked --no-default-features --features rcon --verbose
|
||||||
|
# - cargo test --locked --no-default-features --features rcon,lobby --verbose
|
||||||
|
|
||||||
# Release binaries on GitLab as generic package
|
# Release binaries on GitLab as generic package
|
||||||
release-gitlab-generic-package:
|
release-gitlab-generic-package:
|
||||||
@@ -248,16 +219,16 @@ release-gitlab-generic-package:
|
|||||||
dependencies:
|
dependencies:
|
||||||
- build-x86_64-linux-gnu
|
- build-x86_64-linux-gnu
|
||||||
- build-x86_64-linux-musl
|
- build-x86_64-linux-musl
|
||||||
- build-macos
|
- build-armv7-linux-gnu
|
||||||
|
- build-aarch64-linux-gnu
|
||||||
- build-x86_64-windows
|
- build-x86_64-windows
|
||||||
only:
|
only:
|
||||||
- /^v(\d+\.)*\d+$/
|
- /^v(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/
|
||||||
variables:
|
variables:
|
||||||
LINUX_GNU_BIN: "lazymc-x86_64-unknown-linux-gnu"
|
LINUX_GNU_BIN: "lazymc-x86_64-unknown-linux-gnu"
|
||||||
LINUX_MUSL_BIN: "lazymc-x86_64-unknown-linux-musl"
|
LINUX_MUSL_BIN: "lazymc-x86_64-unknown-linux-musl"
|
||||||
LINUX_ARMV7_GNU_BIN: "lazymc-armv7-unknown-linux-gnueabihf"
|
LINUX_ARMV7_GNU_BIN: "lazymc-armv7-unknown-linux-gnueabihf"
|
||||||
LINUX_AARCH64_GNU_BIN: "lazymc-aarch64-unknown-linux-gnu"
|
LINUX_AARCH64_GNU_BIN: "lazymc-aarch64-unknown-linux-gnu"
|
||||||
MACOS_BIN: "lazymc-x86_64-apple-darwin"
|
|
||||||
WINDOWS_BIN: "lazymc-x86_64-pc-windows-msvc.exe"
|
WINDOWS_BIN: "lazymc-x86_64-pc-windows-msvc.exe"
|
||||||
before_script: []
|
before_script: []
|
||||||
script:
|
script:
|
||||||
@@ -274,8 +245,6 @@ release-gitlab-generic-package:
|
|||||||
curl --header "JOB-TOKEN: ${CI_JOB_TOKEN}" --upload-file ${LINUX_ARMV7_GNU_BIN} ${PACKAGE_REGISTRY_URL}/${LINUX_ARMV7_GNU_BIN}
|
curl --header "JOB-TOKEN: ${CI_JOB_TOKEN}" --upload-file ${LINUX_ARMV7_GNU_BIN} ${PACKAGE_REGISTRY_URL}/${LINUX_ARMV7_GNU_BIN}
|
||||||
- |
|
- |
|
||||||
curl --header "JOB-TOKEN: ${CI_JOB_TOKEN}" --upload-file ${LINUX_AARCH64_GNU_BIN} ${PACKAGE_REGISTRY_URL}/${LINUX_AARCH64_GNU_BIN}
|
curl --header "JOB-TOKEN: ${CI_JOB_TOKEN}" --upload-file ${LINUX_AARCH64_GNU_BIN} ${PACKAGE_REGISTRY_URL}/${LINUX_AARCH64_GNU_BIN}
|
||||||
- |
|
|
||||||
curl --header "JOB-TOKEN: ${CI_JOB_TOKEN}" --upload-file ${MACOS_BIN} ${PACKAGE_REGISTRY_URL}/${MACOS_BIN}
|
|
||||||
- |
|
- |
|
||||||
curl --header "JOB-TOKEN: ${CI_JOB_TOKEN}" --upload-file ${WINDOWS_BIN} ${PACKAGE_REGISTRY_URL}/${WINDOWS_BIN}
|
curl --header "JOB-TOKEN: ${CI_JOB_TOKEN}" --upload-file ${WINDOWS_BIN} ${PACKAGE_REGISTRY_URL}/${WINDOWS_BIN}
|
||||||
|
|
||||||
@@ -284,13 +253,12 @@ release-gitlab-release:
|
|||||||
image: registry.gitlab.com/gitlab-org/release-cli
|
image: registry.gitlab.com/gitlab-org/release-cli
|
||||||
stage: release
|
stage: release
|
||||||
only:
|
only:
|
||||||
- /^v(\d+\.)*\d+$/
|
- /^v(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/
|
||||||
variables:
|
variables:
|
||||||
LINUX_GNU_BIN: "lazymc-x86_64-unknown-linux-gnu"
|
LINUX_GNU_BIN: "lazymc-x86_64-unknown-linux-gnu"
|
||||||
LINUX_MUSL_BIN: "lazymc-x86_64-unknown-linux-musl"
|
LINUX_MUSL_BIN: "lazymc-x86_64-unknown-linux-musl"
|
||||||
LINUX_ARMV7_GNU_BIN: "lazymc-armv7-unknown-linux-gnueabihf"
|
LINUX_ARMV7_GNU_BIN: "lazymc-armv7-unknown-linux-gnueabihf"
|
||||||
LINUX_AARCH64_GNU_BIN: "lazymc-aarch64-unknown-linux-gnu"
|
LINUX_AARCH64_GNU_BIN: "lazymc-aarch64-unknown-linux-gnu"
|
||||||
MACOS_BIN: "lazymc-x86_64-apple-darwin"
|
|
||||||
WINDOWS_BIN: "lazymc-x86_64-pc-windows-msvc.exe"
|
WINDOWS_BIN: "lazymc-x86_64-pc-windows-msvc.exe"
|
||||||
before_script: []
|
before_script: []
|
||||||
script:
|
script:
|
||||||
@@ -305,20 +273,18 @@ release-gitlab-release:
|
|||||||
--assets-link "{\"name\":\"${LINUX_MUSL_BIN}\",\"url\":\"${PACKAGE_REGISTRY_URL}/${LINUX_MUSL_BIN}\"}" \
|
--assets-link "{\"name\":\"${LINUX_MUSL_BIN}\",\"url\":\"${PACKAGE_REGISTRY_URL}/${LINUX_MUSL_BIN}\"}" \
|
||||||
--assets-link "{\"name\":\"${LINUX_ARMV7_GNU_BIN}\",\"url\":\"${PACKAGE_REGISTRY_URL}/${LINUX_ARMV7_GNU_BIN}\"}" \
|
--assets-link "{\"name\":\"${LINUX_ARMV7_GNU_BIN}\",\"url\":\"${PACKAGE_REGISTRY_URL}/${LINUX_ARMV7_GNU_BIN}\"}" \
|
||||||
--assets-link "{\"name\":\"${LINUX_AARCH64_GNU_BIN}\",\"url\":\"${PACKAGE_REGISTRY_URL}/${LINUX_AARCH64_GNU_BIN}\"}" \
|
--assets-link "{\"name\":\"${LINUX_AARCH64_GNU_BIN}\",\"url\":\"${PACKAGE_REGISTRY_URL}/${LINUX_AARCH64_GNU_BIN}\"}" \
|
||||||
--assets-link "{\"name\":\"${MACOS_BIN}\",\"url\":\"${PACKAGE_REGISTRY_URL}/${MACOS_BIN}\"}" \
|
|
||||||
--assets-link "{\"name\":\"${WINDOWS_BIN}\",\"url\":\"${PACKAGE_REGISTRY_URL}/${WINDOWS_BIN}\"}"
|
--assets-link "{\"name\":\"${WINDOWS_BIN}\",\"url\":\"${PACKAGE_REGISTRY_URL}/${WINDOWS_BIN}\"}"
|
||||||
|
|
||||||
# Publish GitHub release
|
# Publish GitHub release
|
||||||
release-github:
|
release-github:
|
||||||
stage: release
|
stage: release
|
||||||
only:
|
only:
|
||||||
- /^v(\d+\.)*\d+$/
|
- /^v(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/
|
||||||
dependencies:
|
dependencies:
|
||||||
- build-x86_64-linux-gnu
|
- build-x86_64-linux-gnu
|
||||||
- build-x86_64-linux-musl
|
- build-x86_64-linux-musl
|
||||||
- build-armv7-linux-gnu
|
- build-armv7-linux-gnu
|
||||||
- build-aarch64-linux-gnu
|
- build-aarch64-linux-gnu
|
||||||
- build-macos
|
|
||||||
- build-x86_64-windows
|
- build-x86_64-windows
|
||||||
before_script: []
|
before_script: []
|
||||||
script:
|
script:
|
||||||
@@ -337,5 +303,4 @@ release-github:
|
|||||||
- ./github-release upload --token "$GITHUB_TOKEN" --owner timvisee --repo lazymc --tag "$CI_COMMIT_REF_NAME" --file ./lazymc-x86_64-unknown-linux-musl --name lazymc-$CI_COMMIT_REF_NAME-linux-x64-static
|
- ./github-release upload --token "$GITHUB_TOKEN" --owner timvisee --repo lazymc --tag "$CI_COMMIT_REF_NAME" --file ./lazymc-x86_64-unknown-linux-musl --name lazymc-$CI_COMMIT_REF_NAME-linux-x64-static
|
||||||
- ./github-release upload --token "$GITHUB_TOKEN" --owner timvisee --repo lazymc --tag "$CI_COMMIT_REF_NAME" --file ./lazymc-armv7-unknown-linux-gnueabihf --name lazymc-$CI_COMMIT_REF_NAME-linux-armv7
|
- ./github-release upload --token "$GITHUB_TOKEN" --owner timvisee --repo lazymc --tag "$CI_COMMIT_REF_NAME" --file ./lazymc-armv7-unknown-linux-gnueabihf --name lazymc-$CI_COMMIT_REF_NAME-linux-armv7
|
||||||
- ./github-release upload --token "$GITHUB_TOKEN" --owner timvisee --repo lazymc --tag "$CI_COMMIT_REF_NAME" --file ./lazymc-aarch64-unknown-linux-gnu --name lazymc-$CI_COMMIT_REF_NAME-linux-aarch64
|
- ./github-release upload --token "$GITHUB_TOKEN" --owner timvisee --repo lazymc --tag "$CI_COMMIT_REF_NAME" --file ./lazymc-aarch64-unknown-linux-gnu --name lazymc-$CI_COMMIT_REF_NAME-linux-aarch64
|
||||||
- ./github-release upload --token "$GITHUB_TOKEN" --owner timvisee --repo lazymc --tag "$CI_COMMIT_REF_NAME" --file ./lazymc-x86_64-apple-darwin --name lazymc-$CI_COMMIT_REF_NAME-macos
|
|
||||||
- ./github-release upload --token "$GITHUB_TOKEN" --owner timvisee --repo lazymc --tag "$CI_COMMIT_REF_NAME" --file ./lazymc-x86_64-pc-windows-msvc.exe --name lazymc-$CI_COMMIT_REF_NAME-windows.exe
|
- ./github-release upload --token "$GITHUB_TOKEN" --owner timvisee --repo lazymc --tag "$CI_COMMIT_REF_NAME" --file ./lazymc-x86_64-pc-windows-msvc.exe --name lazymc-$CI_COMMIT_REF_NAME-windows.exe
|
||||||
|
80
CHANGELOG.md
80
CHANGELOG.md
@@ -1,5 +1,85 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 0.2.10 (2023-02-20)
|
||||||
|
|
||||||
|
- Do not report an error when server exits with status code 143
|
||||||
|
|
||||||
|
## 0.2.9 (2023-02-14)
|
||||||
|
|
||||||
|
- Fix dropping all connections when `server.drop_banned_ips` was enabled
|
||||||
|
- Update dependencies
|
||||||
|
|
||||||
|
## 0.2.8 (2023-01-30)
|
||||||
|
|
||||||
|
- Add `freeze_process` feature on Unix platforms to freeze a sleeping server
|
||||||
|
rather than shutting it down.
|
||||||
|
- Update default Minecraft version to 1.19.3
|
||||||
|
- Remove macOS builds from releases, users can compile from source
|
||||||
|
- Update dependencies
|
||||||
|
|
||||||
|
## 0.2.7 (2021-12-13)
|
||||||
|
|
||||||
|
- Update default Minecraft version to 1.18.1
|
||||||
|
- Update dependencies
|
||||||
|
|
||||||
|
## 0.2.6 (2021-11-28)
|
||||||
|
|
||||||
|
- Add whitelist support, use server whitelist to prevent unknown users from waking server
|
||||||
|
- Update dependencies
|
||||||
|
|
||||||
|
## 0.2.5 (2021-11-25)
|
||||||
|
|
||||||
|
- Add support Minecraft 1.16.3 to 1.17.1 with lobby join method
|
||||||
|
- Add support for Forge client/server to lobby join method (partial)
|
||||||
|
- Probe server on start with fake user to fetch server settings improving compatibility
|
||||||
|
- Improve lobby compatibility, send probed server data to client when possible
|
||||||
|
- Skip lobby join method if server probe is not yet finished
|
||||||
|
- Generate lobby dimension configuration on the fly based on server dimensions
|
||||||
|
- Fix unsupported lobby dimension configuration values for some Minecraft versions
|
||||||
|
- Demote IP ban list reload message from info to debug
|
||||||
|
- Update dependencies
|
||||||
|
|
||||||
|
## 0.2.4 (2021-11-24)
|
||||||
|
|
||||||
|
- Fix status response issues with missing server icon, fall back to default icon
|
||||||
|
- Fix incorrect UUID for players in lobby logic
|
||||||
|
- Make server directory relative to configuration file path
|
||||||
|
- Assume SIGTERM exit code for server process to be successful on Unix
|
||||||
|
- Update features in README
|
||||||
|
- Update dependencies
|
||||||
|
|
||||||
|
## 0.2.3 (2021-11-22)
|
||||||
|
|
||||||
|
- Add support for `PROXY` header to notify Minecraft server of real client IP
|
||||||
|
- Only enable RCON by default on Windows
|
||||||
|
- Update dependencies
|
||||||
|
|
||||||
|
## 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
|
||||||
|
- Add option to configure list of join methods to occupy client with while server is starting (kick, hold, forward, lobby)
|
||||||
|
- Add lobby join method, keeps client in lobby world on emulated server, teleports to real server when it is ready (highly experimental)
|
||||||
|
- Add forward join method to forward (proxy) client to other host while server is starting
|
||||||
|
- Restructure `lazymc.toml` configuration
|
||||||
|
- Increase packet reading buffer size to speed things up
|
||||||
|
- Add support for Minecraft packet compression
|
||||||
|
- Show warning if config version is outdated or invalid
|
||||||
|
- Various fixes and improvements
|
||||||
|
|
||||||
|
## 0.1.3 (2021-11-15)
|
||||||
|
|
||||||
|
- Fix binary release
|
||||||
|
|
||||||
## 0.1.2 (2021-11-15)
|
## 0.1.2 (2021-11-15)
|
||||||
|
|
||||||
- Add Linux ARMv7 and aarch64 releases
|
- Add Linux ARMv7 and aarch64 releases
|
||||||
|
1290
Cargo.lock
generated
1290
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
83
Cargo.toml
83
Cargo.toml
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "lazymc"
|
name = "lazymc"
|
||||||
version = "0.1.2"
|
version = "0.2.10"
|
||||||
authors = ["Tim Visee <3a4fb3964f@sinenomine.email>"]
|
authors = ["Tim Visee <3a4fb3964f@sinenomine.email>"]
|
||||||
license = "GPL-3.0"
|
license = "GPL-3.0"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
@@ -8,43 +8,90 @@ homepage = "https://timvisee.com/projects/lazymc"
|
|||||||
repository = "https://gitlab.com/timvisee/lazymc"
|
repository = "https://gitlab.com/timvisee/lazymc"
|
||||||
description = "Put your Minecraft server to rest when idle."
|
description = "Put your Minecraft server to rest when idle."
|
||||||
keywords = ["minecraft", "server", "idle", "cli"]
|
keywords = ["minecraft", "server", "idle", "cli"]
|
||||||
categories = [
|
categories = ["command-line-interface", "games"]
|
||||||
"command-line-interface",
|
exclude = ["/.github", "/contrib"]
|
||||||
"games",
|
|
||||||
]
|
|
||||||
exclude = [
|
|
||||||
"/.github",
|
|
||||||
"/contrib",
|
|
||||||
]
|
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
|
[profile.release]
|
||||||
|
codegen-units = 1
|
||||||
|
lto = true
|
||||||
|
strip = true
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["rcon"]
|
default = ["rcon", "lobby"]
|
||||||
rcon = ["rust_rcon"]
|
|
||||||
|
# RCON support
|
||||||
|
# Allow use of RCON to manage (stop) server.
|
||||||
|
# Required on Windows.
|
||||||
|
rcon = ["rust_rcon", "async-std"]
|
||||||
|
|
||||||
|
# Lobby support
|
||||||
|
# Add lobby join method, keeps client in fake lobby world until server is ready.
|
||||||
|
lobby = ["md-5", "uuid"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow = "1.0"
|
anyhow = "1.0"
|
||||||
|
base64 = "0.21"
|
||||||
bytes = "1.1"
|
bytes = "1.1"
|
||||||
clap = { version = "3.0.0-beta.5", default-features = false, features = [ "std", "cargo", "color", "env", "suggestions", "unicode" ]}
|
chrono = "0.4"
|
||||||
|
clap = { version = "4.0.32", default-features = false, features = [
|
||||||
|
"std",
|
||||||
|
"help",
|
||||||
|
"suggestions",
|
||||||
|
"color",
|
||||||
|
"usage",
|
||||||
|
"cargo",
|
||||||
|
"env",
|
||||||
|
"unicode",
|
||||||
|
] }
|
||||||
colored = "2.0"
|
colored = "2.0"
|
||||||
derive_builder = "0.10"
|
derive_builder = "0.12"
|
||||||
dotenv = "0.15"
|
dotenv = "0.15"
|
||||||
futures = { version = "0.3", default-features = false }
|
flate2 = { version = "1.0", default-features = false, features = ["default"] }
|
||||||
|
futures = { version = "0.3", default-features = false, features = ["executor"] }
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
minecraft-protocol = { git = "https://github.com/timvisee/rust-minecraft-protocol", rev = "31041b8" }
|
minecraft-protocol = { git = "https://git.cozy.software/slime/rust-minecraft-protocol", rev = "4d9c0d4305" }
|
||||||
|
named-binary-tag = "0.6"
|
||||||
|
nix = "0.26"
|
||||||
|
notify = "4.0"
|
||||||
pretty_env_logger = "0.4"
|
pretty_env_logger = "0.4"
|
||||||
|
proxy-protocol = "0.5"
|
||||||
|
quartz_nbt = "0.2"
|
||||||
rand = "0.8"
|
rand = "0.8"
|
||||||
serde = "1.0"
|
serde = "1.0"
|
||||||
|
serde_json = "1.0"
|
||||||
shlex = "1.1"
|
shlex = "1.1"
|
||||||
thiserror = "1.0"
|
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"
|
toml = "0.5"
|
||||||
|
version-compare = "0.1"
|
||||||
|
|
||||||
# Feature: rcon
|
# Feature: rcon
|
||||||
rust_rcon = { package = "rcon", version = "0.5", optional = true }
|
rust_rcon = { package = "rcon", version = "0.5.2", optional = true }
|
||||||
|
async-std = { version = "1.9.0", default-features = false, optional = true }
|
||||||
|
|
||||||
|
# Feature: lobby
|
||||||
|
md-5 = { version = "0.10", optional = true }
|
||||||
|
uuid = { version = "0.7", optional = true, features = ["v3"] }
|
||||||
|
|
||||||
[target.'cfg(unix)'.dependencies]
|
[target.'cfg(unix)'.dependencies]
|
||||||
libc = "0.2"
|
libc = "0.2"
|
||||||
|
|
||||||
[target.'cfg(windows)'.dependencies]
|
[target.'cfg(windows)'.dependencies]
|
||||||
winapi = { version = "0.3", features = ["winuser", "processthreadsapi", "handleapi", "ntdef", "minwindef"] }
|
winapi = { version = "0.3", features = [
|
||||||
|
"winuser",
|
||||||
|
"processthreadsapi",
|
||||||
|
"handleapi",
|
||||||
|
"ntdef",
|
||||||
|
"minwindef",
|
||||||
|
] }
|
||||||
|
42
README.md
42
README.md
@@ -35,12 +35,19 @@ https://user-images.githubusercontent.com/856222/141378688-882082be-9efa-4cfe-81
|
|||||||
## Features
|
## Features
|
||||||
|
|
||||||
- Very efficient, lightweight & low-profile (~3KB RAM)
|
- Very efficient, lightweight & low-profile (~3KB RAM)
|
||||||
- Supports Minecraft Java Edition 1.6+, supports modded (e.g. Forge, FTB)
|
- Supports Minecraft Java Edition 1.7.2+, supports modded (e.g. Forge, FTB)
|
||||||
- Transparent join: hold clients when server starts, relay when ready, without them noticing
|
- Configure joining client occupation methods:
|
||||||
|
- Hold: hold clients when server starts, relay when ready, without them noticing
|
||||||
|
- Kick: kick clients when server starts, with a starting message
|
||||||
|
- Forward: forward client to another IP when server starts
|
||||||
|
- _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
|
- Customizable MOTD and login messages
|
||||||
- Automatically manages `server.properties` (host, port and RCON settings)
|
- Automatically manages `server.properties` (host, port and RCON settings)
|
||||||
- Graceful server sleep/shutdown through RCON (with `SIGTERM` fallback on Linux/Unix)
|
- Automatically block banned IPs from server within lazymc
|
||||||
|
- Graceful server sleep/shutdown through RCON or `SIGTERM`
|
||||||
|
- Real client IP on Minecraft server with `PROXY` header ([usage](./docs/proxy-ip.md))
|
||||||
- Restart server on crash
|
- Restart server on crash
|
||||||
|
- Lockout mode
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
@@ -48,6 +55,10 @@ https://user-images.githubusercontent.com/856222/141378688-882082be-9efa-4cfe-81
|
|||||||
- Minecraft Java Edition 1.6+
|
- Minecraft Java Edition 1.6+
|
||||||
- On Windows: RCON (automatically managed)
|
- On Windows: RCON (automatically managed)
|
||||||
|
|
||||||
|
Build requirements:
|
||||||
|
|
||||||
|
- Rust 1.64 (MSRV)
|
||||||
|
|
||||||
_Note: You must have access to the system to run the `lazymc` binary. If you're
|
_Note: You must have access to the system to run the `lazymc` binary. If you're
|
||||||
using a Minecraft shared hosting provider with a custom dashboard, you likely
|
using a Minecraft shared hosting provider with a custom dashboard, you likely
|
||||||
won't be able to set this up._
|
won't be able to set this up._
|
||||||
@@ -60,7 +71,8 @@ _Note: these instructions are for Linux & macOS, for Windows look
|
|||||||
Make sure you meet all [requirements](#requirements).
|
Make sure you meet all [requirements](#requirements).
|
||||||
|
|
||||||
Download the appropriate binary for your system from the [latest
|
Download the appropriate binary for your system from the [latest
|
||||||
release][latest-release] page.
|
release][latest-release] page. On macOS you must [compile from
|
||||||
|
source](#compile-from-source).
|
||||||
|
|
||||||
Place the binary in your Minecraft server directory, rename it if you like.
|
Place the binary in your Minecraft server directory, rename it if you like.
|
||||||
Open a terminal, go to the directory, and make sure you can invoke it:
|
Open a terminal, go to the directory, and make sure you can invoke it:
|
||||||
@@ -70,8 +82,8 @@ chmod a+x ./lazymc
|
|||||||
./lazymc --help
|
./lazymc --help
|
||||||
```
|
```
|
||||||
|
|
||||||
When `lazymc` is set-up, change into your server directory if you haven't
|
When lazymc is set-up, change into your server directory if you haven't already.
|
||||||
already. Then set up the [configuration](./res/lazymc.toml) and start it up:
|
Then set up the [configuration](./res/lazymc.toml) and start it up:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Change into your server directory (if you haven't already)
|
# Change into your server directory (if you haven't already)
|
||||||
@@ -88,20 +100,14 @@ nano lazymc.toml
|
|||||||
lazymc start
|
lazymc start
|
||||||
```
|
```
|
||||||
|
|
||||||
Before you use this in production, please ensure starting and stopping the
|
Please see [extras](./docs/extras.md) for recommendations and additional things
|
||||||
server works as expected by connecting to it once. Watch `lazymc`s output while
|
to set up (e.g. how to fix incorrect client IPs and IP banning on your server).
|
||||||
it starts and stops. If stopping results in errors, fix this first to prevent
|
|
||||||
corrupting world/user data.
|
|
||||||
|
|
||||||
Follow this repository with the _Watch_ button on the top right to be notified of new releases.
|
After you've read through the [extras](./docs/extras.md), everything should now
|
||||||
|
be ready to go! Connect with your Minecraft client to wake your server up!
|
||||||
Everything should now be ready to go! Connect with your Minecraft client to wake
|
|
||||||
your server up!
|
|
||||||
|
|
||||||
_Note: If a binary for your system isn't provided, please [compile from
|
_Note: If a binary for your system isn't provided, please [compile from
|
||||||
source](#compile-from-source)._
|
source](#compile-from-source). Installation options are limited at this moment. More will be added
|
||||||
|
|
||||||
_Note: Installation options are limited at this moment. More will be added
|
|
||||||
later._
|
later._
|
||||||
|
|
||||||
[latest-release]: https://github.com/timvisee/lazymc/releases/latest
|
[latest-release]: https://github.com/timvisee/lazymc/releases/latest
|
||||||
@@ -139,7 +145,7 @@ cargo build --release
|
|||||||
|
|
||||||
## Third-party usage & implementations
|
## Third-party usage & implementations
|
||||||
|
|
||||||
A list of third-party implementations, projects using `lazymc`, that you might
|
A list of third-party implementations, projects using lazymc, that you might
|
||||||
find useful:
|
find useful:
|
||||||
|
|
||||||
- Docker: [crbanman/papermc-lazymc](https://hub.docker.com/r/crbanman/papermc-lazymc) _(PaperMC with lazymc in Docker)_
|
- Docker: [crbanman/papermc-lazymc](https://hub.docker.com/r/crbanman/papermc-lazymc) _(PaperMC with lazymc in Docker)_
|
||||||
|
13
TODO.md
13
TODO.md
@@ -9,14 +9,19 @@
|
|||||||
|
|
||||||
- Use server whitelist/blacklist
|
- Use server whitelist/blacklist
|
||||||
- Console error if server already started on port, not through `lazymc`
|
- Console error if server already started on port, not through `lazymc`
|
||||||
- Kick with message if proxy-to-server connection fails for new client.
|
- Kick with message if proxy-to-server connection fails for new client.
|
||||||
- Test configuration on start (server dir exists, command not empty)
|
- Test configuration on start (server dir exists, command not empty)
|
||||||
- Also quit `lazymc` after CTRL+C signal, after server has stopped
|
|
||||||
- Dynamically increase/decrease server polling interval based on server state
|
- Dynamically increase/decrease server polling interval based on server state
|
||||||
- Server polling through query (`enable-query` in `server.properties`, uses GameSpy4 protocol)
|
- Server polling through query (`enable-query` in `server.properties`, uses GameSpy4 protocol)
|
||||||
|
|
||||||
## Experiment
|
## Experiment
|
||||||
|
|
||||||
- Lobby method: let players connect with an emulated empty server (like 2b2t's
|
|
||||||
queue), redirect them when the server started.
|
|
||||||
- `io_uring` on Linux for efficient proxying (see `tokio-uring`)
|
- `io_uring` on Linux for efficient proxying (see `tokio-uring`)
|
||||||
|
|
||||||
|
## Lobby join method
|
||||||
|
|
||||||
|
- add support for more Minecraft versions (with changed protocols)
|
||||||
|
- support online mode (encryption)
|
||||||
|
- hold back packets (whitelist), forward to server at connect before joining
|
||||||
|
- add support for forge (emulate mod list communication)
|
||||||
|
- on login plugin request during login state, respond with empty payload, not supported
|
||||||
|
4
build.rs
4
build.rs
@@ -1,7 +1,7 @@
|
|||||||
fn main() {
|
fn main() {
|
||||||
// Must enable rcon on Windows
|
// rcon is required on Windows
|
||||||
#[cfg(all(windows, not(feature = "rcon")))]
|
#[cfg(all(windows, not(feature = "rcon")))]
|
||||||
{
|
{
|
||||||
println!("cargo:warning=lazymc: you must enable rcon feature on Windows");
|
compile_error!("required feature missing on Windows: rcon");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
1
clippy.toml
Normal file
1
clippy.toml
Normal file
@@ -0,0 +1 @@
|
|||||||
|
msrv = "1.64.0"
|
41
docs/command-bash.md
Normal file
41
docs/command-bash.md
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
# Use bash script to start server
|
||||||
|
|
||||||
|
You may use a `bash` script to start your server rather than invoking `java`
|
||||||
|
directly. This requires some changes though to ensure your server properly shuts
|
||||||
|
down.
|
||||||
|
|
||||||
|
When lazymc stops your server it sends a [`SIGTERM`][sigterm] signal to the
|
||||||
|
invoked server process to gracefully shut it down. `bash` ignores this signal by
|
||||||
|
default and keeps the Minecraft server running.
|
||||||
|
|
||||||
|
You must configure `bash` to [forward][forward-signal] the signal to properly
|
||||||
|
shutdown the Minecraft server as well.
|
||||||
|
|
||||||
|
[sigterm]: https://en.wikipedia.org/wiki/Signal_(IPC)#SIGTERM
|
||||||
|
[forward-signal]: https://unix.stackexchange.com/a/434269/61092
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
Here's a minimal example, trapping the signal and forwarding it to the server.
|
||||||
|
Be sure to set the correct server JAR file and appropriate memory limits.
|
||||||
|
|
||||||
|
[`start-server`](../res/start-server):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Server JAR file, set this to your own
|
||||||
|
FILE=server.jar
|
||||||
|
|
||||||
|
# Trap SIGTERM, forward it to server process ID
|
||||||
|
trap 'kill -TERM $PID' TERM INT
|
||||||
|
|
||||||
|
# Start server
|
||||||
|
java -Xms1G -Xmx1G -jar $FILE --nogui &
|
||||||
|
|
||||||
|
# Remember server process ID, wait for it to quit, then reset the trap
|
||||||
|
PID=$!
|
||||||
|
wait $PID
|
||||||
|
trap - TERM INT
|
||||||
|
wait $PID
|
||||||
|
```
|
@@ -1,41 +0,0 @@
|
|||||||
# Use bash script to start server
|
|
||||||
|
|
||||||
You may use a `bash` script to start your server rather than invoking `java`
|
|
||||||
directly. This requires some changes though to ensure your server properly shuts
|
|
||||||
down.
|
|
||||||
|
|
||||||
When `lazymc` stops your server it sends a [`SIGTERM`][sigterm] signal to the
|
|
||||||
invoked server process to gracefully shut it down. `bash` ignores this signal by
|
|
||||||
default and keeps the Minecraft server running.
|
|
||||||
|
|
||||||
You must configure `bash` to [forward][forward-signal] the signal to properly
|
|
||||||
shutdown the Minecraft server as well.
|
|
||||||
|
|
||||||
[sigterm]: https://en.wikipedia.org/wiki/Signal_(IPC)#SIGTERM
|
|
||||||
[forward-signal]: https://unix.stackexchange.com/a/434269/61092
|
|
||||||
|
|
||||||
## Example
|
|
||||||
|
|
||||||
Here's a minimal example, trapping the signal and forwarding it to the server.
|
|
||||||
Be sure to set the correct server JAR file and appropriate memory limits.
|
|
||||||
|
|
||||||
[`start-server`](../res/start-server):
|
|
||||||
|
|
||||||
```bash
|
|
||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# Server JAR file, set this to your own
|
|
||||||
FILE=server.jar
|
|
||||||
|
|
||||||
# Trap SIGTERM, forward it to server process ID
|
|
||||||
trap 'kill -TERM $PID' TERM INT
|
|
||||||
|
|
||||||
# Start server
|
|
||||||
java -Xms1G -Xmx1G -jar $FILE --nogui &
|
|
||||||
|
|
||||||
# Remember server process ID, wait for it to quit, then reset the trap
|
|
||||||
PID=$!
|
|
||||||
wait $PID
|
|
||||||
trap - TERM INT
|
|
||||||
wait $PID
|
|
||||||
```
|
|
1
docs/command_bash.md
Symbolic link
1
docs/command_bash.md
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
command-bash.md
|
28
docs/extras.md
Normal file
28
docs/extras.md
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
# Extras
|
||||||
|
|
||||||
|
Some extra steps and recommendations when using lazymc:
|
||||||
|
|
||||||
|
Before you use this in production, always ensure starting and stopping the
|
||||||
|
server works as expected by connecting to it once. Watch lazymc's output while
|
||||||
|
it starts and stops. If stopping results in errors, fix this first to prevent
|
||||||
|
corrupting world/user data.
|
||||||
|
|
||||||
|
Follow this repository with the _Watch_ button on the top right to be notified
|
||||||
|
of new releases.
|
||||||
|
|
||||||
|
## Recommended
|
||||||
|
|
||||||
|
- [Protocol version](./protocol-version.md):
|
||||||
|
_set correct Minecraft protocol version for the best client compatability_
|
||||||
|
- [Proxy IP](./proxy-ip.md):
|
||||||
|
_fix incorrect client IPs on Minecraft server, notify server of correct IP with `PROXY` header_
|
||||||
|
|
||||||
|
## Tips
|
||||||
|
|
||||||
|
- [bash with start command](./command_bash.md):
|
||||||
|
_how to properly use a bash script as server start command_
|
||||||
|
|
||||||
|
## Experimental features
|
||||||
|
|
||||||
|
- [Join method: lobby](./join-method-lobby.md):
|
||||||
|
_keep clients in fake lobby world while server starts, teleport to real server when ready_
|
114
docs/join-method-lobby.md
Normal file
114
docs/join-method-lobby.md
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
# Join method: lobby
|
||||||
|
|
||||||
|
**Note: this is highly experimental, incomplete, and may break your game. See
|
||||||
|
[warning](#warning).**
|
||||||
|
|
||||||
|
The lobby join method allows you to keep clients in a lobby world while the
|
||||||
|
server is starting. When the server is ready, the player is _teleported_ to the
|
||||||
|
real server.
|
||||||
|
|
||||||
|
lazymc emulates a fake server with an empty lobby world. The player is put in
|
||||||
|
this world, floating in space. A custom message is shown on the client to notify
|
||||||
|
we're waiting on the server to start.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Warning
|
||||||
|
|
||||||
|
This feature is highly experimental, incomplete and unstable. This may break the
|
||||||
|
game and crash clients. Don't use this unless you know what you're doing. Never
|
||||||
|
enable this in a production environment.
|
||||||
|
|
||||||
|
Current limitations:
|
||||||
|
|
||||||
|
- Server must be in offline mode (`online-mode=false`)
|
||||||
|
- Server must use Minecraft version 1.16.3 to 1.17.1 (tested with 1.17.1)
|
||||||
|
- Server must use vanilla Minecraft
|
||||||
|
- May work with Forge (set `server.forge = true`), depends on used mods, test before use
|
||||||
|
- Does not work with other mods, such as FTB
|
||||||
|
- This method will consume the client, following configured join methods won't be used.
|
||||||
|
|
||||||
|
At this time it is unknown if some of the above limitations will ever be lifted,
|
||||||
|
or if this will ever be implemented in a robust manner.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
_Note: you must use `lazymc v0.2.0` or above with the `lobby` feature enabled._
|
||||||
|
|
||||||
|
To try this out, simply add the `"lobby"` method to the `join.methods` list in
|
||||||
|
your `lazymc.toml` configuration file:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
# -- snip --
|
||||||
|
|
||||||
|
[join]
|
||||||
|
methods = [
|
||||||
|
"lobby",
|
||||||
|
"kick",
|
||||||
|
]
|
||||||
|
|
||||||
|
# -- snip --
|
||||||
|
```
|
||||||
|
|
||||||
|
Then configure the lobby to your likings:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
# -- snip --
|
||||||
|
|
||||||
|
[join.lobby]
|
||||||
|
# Lobby occupation method.
|
||||||
|
# The client joins a fake lobby server with an empty world, floating in space.
|
||||||
|
# A message is overlayed on screen to notify the server is starting.
|
||||||
|
# The client will be teleported to the real server once it is ready.
|
||||||
|
# This may keep the client occupied forever if no timeout is set.
|
||||||
|
# Consumes client, not allowing other join methods afterwards.
|
||||||
|
# See: https://git.io/JMIi4
|
||||||
|
|
||||||
|
# !!! WARNING !!!
|
||||||
|
# This is highly experimental, incomplete and unstable.
|
||||||
|
# This may break the game and crash clients.
|
||||||
|
# Don't enable this unless you know what you're doing.
|
||||||
|
#
|
||||||
|
# - Server must be in offline mode
|
||||||
|
# - Server must use Minecraft version 1.16.3 to 1.17.1 (tested with 1.17.1)
|
||||||
|
# - Server must use vanilla Minecraft
|
||||||
|
# - May work with Forge, enable in config, depends on used mods, test before use
|
||||||
|
# - Does not work with other mods, such as FTB
|
||||||
|
|
||||||
|
# Maximum time in seconds in the lobby while the server starts.
|
||||||
|
timeout = 600
|
||||||
|
|
||||||
|
# Message banner in lobby shown to client.
|
||||||
|
message = "§2Server is starting\n§7⌛ Please wait..."
|
||||||
|
|
||||||
|
# Sound effect to play when server is ready.
|
||||||
|
ready_sound = "block.note_block.chime"
|
||||||
|
|
||||||
|
# -- snip --
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
_Note: this might have changed, see the latest configuration
|
||||||
|
[here](../res/lazymc.toml)._
|
||||||
|
|
||||||
|
## Probe issue with whitelist
|
||||||
|
|
||||||
|
lazymc may report a _probe_ error on first start when a whitelist is enabled
|
||||||
|
on your server.
|
||||||
|
|
||||||
|
lazymc uses a probe to fetch some required details from your Minecraft
|
||||||
|
server, such as a mod list. When probing, the server is started once when lazymc
|
||||||
|
starts. It then connects to the Minecraft server with the _probe_ user
|
||||||
|
(username: `_lazymc_probe`) and disconnects when everything needed is fetched.
|
||||||
|
|
||||||
|
If you use a whitelist on your server it will cause issues if the probe user
|
||||||
|
isn't whitelisted. Simply whitelist the probe user with the following command
|
||||||
|
and restart lazymc to fix the issue:
|
||||||
|
|
||||||
|
```
|
||||||
|
/whitelist add _lazymc_probe
|
||||||
|
```
|
||||||
|
|
||||||
|
Probing isn't enabled by default. You may enable this by setting
|
||||||
|
`server.probe_on_start = true`. Other configuration settings might
|
||||||
|
automatically enable proving if required for your setup.
|
39
docs/protocol-version.md
Normal file
39
docs/protocol-version.md
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
# Protocol version
|
||||||
|
|
||||||
|
The Minecraft protocol uses a version number to distinguish between different
|
||||||
|
protocol versions. Each new Minecraft version having a change in its protocol
|
||||||
|
gets a new protocol version.
|
||||||
|
|
||||||
|
## List of versions
|
||||||
|
|
||||||
|
- https://wiki.vg/Protocol_version_numbers#Versions_after_the_Netty_rewrite
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
In lazymc you may configure what protocol version to use:
|
||||||
|
|
||||||
|
[`lazymc.toml`](../res/lazymc.toml):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# -- snip --
|
||||||
|
|
||||||
|
[public]
|
||||||
|
# Server version & protocol hint.
|
||||||
|
# Sent to clients until actual server version is known.
|
||||||
|
# See: https://git.io/J1Fvx
|
||||||
|
version = "1.19.3"
|
||||||
|
protocol = 761
|
||||||
|
|
||||||
|
# -- snip --
|
||||||
|
```
|
||||||
|
|
||||||
|
It is highly recommended to set these to match that of your server version to
|
||||||
|
allow the best compatibility with clients.
|
||||||
|
|
||||||
|
- Set `public.protocol` to the number matching that of your server version
|
||||||
|
(see [this](#list-of-versions) list)
|
||||||
|
- Set `public.version` to any string you like. Shows up in read in clients that
|
||||||
|
have an incompatibel protocol version number
|
||||||
|
|
||||||
|
These are used as hint. lazymc will automatically use the protocol version of
|
||||||
|
your Minecraft server once it has started at least once.
|
79
docs/proxy-ip.md
Normal file
79
docs/proxy-ip.md
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
# Proxy IP
|
||||||
|
|
||||||
|
lazymc acts as a proxy most of the time. Because of this the Minecraft server
|
||||||
|
will think all clients connect from the same IP, being the IP lazymc proxies
|
||||||
|
from.
|
||||||
|
|
||||||
|
This breaks IP banning (`/ban-ip`, amongst other IP related things). This may be
|
||||||
|
a problematic issue for your server.
|
||||||
|
|
||||||
|
Luckily, this can be fixed with the [proxy header](#proxy-header). lazymc has
|
||||||
|
support for this, and can be used with a companion plugin on your server.
|
||||||
|
|
||||||
|
## Proxy header
|
||||||
|
|
||||||
|
The `PROXY` header may be used to notify the Minecraft server of the real client
|
||||||
|
IP.
|
||||||
|
|
||||||
|
When a new connection is opened to the Minecraft server, the Minecraft server
|
||||||
|
will read the `PROXY` header with client-IP information. Once read, it will set
|
||||||
|
the correct client IP internally and will resume communicating with the client
|
||||||
|
normally.
|
||||||
|
|
||||||
|
To enable this with lazymc you must do two things:
|
||||||
|
- [Modify the lazymc configuration](#configuration)
|
||||||
|
- [Install a companion plugin](#server-plugin)
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
To use the `PROXY` header with your Minecraft server, set `server.send_proxy_v2`
|
||||||
|
to `true`.
|
||||||
|
|
||||||
|
[`lazymc.toml`](../res/lazymc.toml):
|
||||||
|
|
||||||
|
```toml
|
||||||
|
# -- snip --
|
||||||
|
|
||||||
|
[server]
|
||||||
|
send_proxy_v2 = true
|
||||||
|
|
||||||
|
# -- snip --
|
||||||
|
```
|
||||||
|
|
||||||
|
Other related properties, you probably won't need to touch, include:
|
||||||
|
|
||||||
|
- `server.send_proxy_v2`: set to `true` to enable `PROXY` header for Minecraft server
|
||||||
|
- `join.forward.send_proxy_v2`: set to `true` to enable `PROXY` header forwarded server, if `forward` join method is used
|
||||||
|
- `rcon.send_proxy_v2`: set to `true` to enable `PROXY` header for RCON connections for Minecraft server
|
||||||
|
|
||||||
|
## Server plugin
|
||||||
|
|
||||||
|
Install one of these plugins as companion on your server to enable support for
|
||||||
|
the `PROXY` header. This requires Minecraft server software supporting plugins,
|
||||||
|
the vanilla Minecraft server does not support this.
|
||||||
|
|
||||||
|
If lazymc connects to a Spigot compatible server, use any of:
|
||||||
|
|
||||||
|
- https://github.com/riku6460/SpigotProxy ([JAR](https://github.com/riku6460/SpigotProxy/releases/latest))
|
||||||
|
- https://github.com/timvisee/spigot-proxy
|
||||||
|
|
||||||
|
If lazymc connects to a BungeeCord server, use any of:
|
||||||
|
|
||||||
|
- https://github.com/MinelinkNetwork/BungeeProxy
|
||||||
|
|
||||||
|
## Warning: connection failures
|
||||||
|
|
||||||
|
Use of the `PROXY` header must be enabled or disabled on both lazymc and your
|
||||||
|
Minecraft server using a companion plugin.
|
||||||
|
|
||||||
|
If either of the two is missing or misconfigured, it will result in connection
|
||||||
|
failures due to a missing or unrecognized header.
|
||||||
|
|
||||||
|
## Warning: fake IP
|
||||||
|
|
||||||
|
When enabling the `PROXY` header on your Minecraft server, malicious parties may
|
||||||
|
send this header to fake their real IP.
|
||||||
|
|
||||||
|
To solve this, make sure the Minecraft server is only publicly reachable through
|
||||||
|
lazymc. This can be done by setting the Minecraft server IP to a local address
|
||||||
|
only, or by setting up firewall rules.
|
@@ -14,8 +14,8 @@ Open a terminal, go to the server directory, and make sure you can execute it:
|
|||||||
.\lazymc --help
|
.\lazymc --help
|
||||||
```
|
```
|
||||||
|
|
||||||
When `lazymc` is ready, set up the [configuration](./res/lazymc.toml) and start
|
When lazymc is ready, set up the [configuration](../res/lazymc.toml) and start it
|
||||||
it up:
|
up:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# In your Minecraft server directory:
|
# In your Minecraft server directory:
|
||||||
@@ -31,18 +31,14 @@ notepad lazymc.toml
|
|||||||
.\lazymc start
|
.\lazymc start
|
||||||
```
|
```
|
||||||
|
|
||||||
Before you use this in production, please ensure starting and stopping the
|
Please see [extras](./extras.md) for recommendations and additional things
|
||||||
server works as expected by connecting to it once. Watch `lazymc`s output while
|
to set up (e.g. how to fix incorrect client IPs and IP banning on your server).
|
||||||
it starts and stops. If stopping results in errors, fix this first to prevent
|
|
||||||
corrupting world/user data.
|
|
||||||
|
|
||||||
Follow this repository with the _Watch_ button on the top right to be notified of new releases.
|
After you've read through the [extras](./extras.md), everything should now
|
||||||
|
be ready to go! Connect with your Minecraft client to wake your server up!
|
||||||
Everything should now be ready to go! Connect with your Minecraft client to wake
|
|
||||||
your server up!
|
|
||||||
|
|
||||||
_Note: if you put `lazymc` in `PATH`, or if you
|
_Note: if you put `lazymc` in `PATH`, or if you
|
||||||
[install](../README.md#compile-from-source) it through Cargo, you can invoke
|
[install](../README.md#compile-from-source) it through Cargo, you can invoke
|
||||||
`lazymc` everywhere directly without the `.\` prefix.
|
`lazymc` everywhere directly without the `.\` prefix._
|
||||||
|
|
||||||
[latest-release]: https://github.com/timvisee/lazymc/releases/latest
|
[latest-release]: https://github.com/timvisee/lazymc/releases/latest
|
||||||
|
18
res/dimension.snbt
Normal file
18
res/dimension.snbt
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
piglin_safe: 1b,
|
||||||
|
natural: 0b,
|
||||||
|
ambient_light: 0.0f,
|
||||||
|
fixed_time: 0,
|
||||||
|
infiniburn: "minecraft:infiniburn_overworld",
|
||||||
|
respawn_anchor_works: 0b,
|
||||||
|
has_skylight: 1b,
|
||||||
|
bed_works: 0b,
|
||||||
|
effects: "minecraft:the_end",
|
||||||
|
has_raids: 0b,
|
||||||
|
min_y: 0,
|
||||||
|
height: 256,
|
||||||
|
logical_height: 256,
|
||||||
|
coordinate_scale: 1.0d,
|
||||||
|
ultrawarm: 0b,
|
||||||
|
has_ceiling: 0b
|
||||||
|
}
|
2093
res/dimension_codec.snbt
Normal file
2093
res/dimension_codec.snbt
Normal file
File diff suppressed because it is too large
Load Diff
151
res/lazymc.toml
151
res/lazymc.toml
@@ -8,7 +8,7 @@
|
|||||||
# You can probably leave the rest as-is.
|
# You can probably leave the rest as-is.
|
||||||
#
|
#
|
||||||
# You may generate a new configuration with: lazymc config generate
|
# You may generate a new configuration with: lazymc config generate
|
||||||
# Or find the latest at: https://is.gd/WWBIQu
|
# Or find the latest at: https://git.io/J1Fvq
|
||||||
|
|
||||||
[public]
|
[public]
|
||||||
# Public address. IP and port users connect to.
|
# Public address. IP and port users connect to.
|
||||||
@@ -17,9 +17,9 @@
|
|||||||
|
|
||||||
# Server version & protocol hint.
|
# Server version & protocol hint.
|
||||||
# Sent to clients until actual server version is known.
|
# Sent to clients until actual server version is known.
|
||||||
# See: https://is.gd/FTQKTP
|
# See: https://git.io/J1Fvx
|
||||||
#version = "1.17.1"
|
#version = "1.19.3"
|
||||||
#protocol = 756
|
#protocol = 761
|
||||||
|
|
||||||
[server]
|
[server]
|
||||||
# Server address. Internal IP and port of server started by lazymc to proxy to.
|
# Server address. Internal IP and port of server started by lazymc to proxy to.
|
||||||
@@ -30,15 +30,45 @@
|
|||||||
directory = "."
|
directory = "."
|
||||||
|
|
||||||
# Command to start the server.
|
# Command to start the server.
|
||||||
# Warning: if using a bash script read: https://is.gd/k8SQYv
|
# Warning: if using a bash script read: https://git.io/JMIKH
|
||||||
command = "java -Xmx1G -Xms1G -jar server.jar --nogui"
|
command = "java -Xmx1G -Xms1G -jar server.jar --nogui"
|
||||||
|
|
||||||
|
# Freeze the server process instead of restarting it when no players online, making it resume faster.
|
||||||
|
# Only works on Unix (Linux or MacOS), ignored on Windows
|
||||||
|
#freeze_process = true
|
||||||
|
|
||||||
# Immediately wake server when starting lazymc.
|
# Immediately wake server when starting lazymc.
|
||||||
#wake_on_start = false
|
#wake_on_start = false
|
||||||
|
|
||||||
# Immediately wake server after crash.
|
# Immediately wake server after crash.
|
||||||
#wake_on_crash = false
|
#wake_on_crash = false
|
||||||
|
|
||||||
|
# Probe required server details when starting lazymc, wakes server on start.
|
||||||
|
# Improves client compatibility. Automatically enabled if required by other config properties.
|
||||||
|
#probe_on_start = false
|
||||||
|
|
||||||
|
# Set to true if this server runs Forge.
|
||||||
|
#forge = false
|
||||||
|
|
||||||
|
# Server start/stop timeout in seconds. Force kill server process if it takes too long.
|
||||||
|
#start_timeout = 300
|
||||||
|
#stop_timeout = 150
|
||||||
|
|
||||||
|
# To wake server, user must be in server whitelist if enabled on server.
|
||||||
|
#wake_whitelist = true
|
||||||
|
|
||||||
|
# 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
|
||||||
|
|
||||||
|
# Add HAProxy v2 header to proxied connections.
|
||||||
|
# See: https://git.io/J1bYb
|
||||||
|
#send_proxy_v2 = false
|
||||||
|
|
||||||
[time]
|
[time]
|
||||||
# Sleep after number of seconds.
|
# Sleep after number of seconds.
|
||||||
#sleep_after = 60
|
#sleep_after = 60
|
||||||
@@ -46,31 +76,97 @@ command = "java -Xmx1G -Xms1G -jar server.jar --nogui"
|
|||||||
# Minimum time in seconds to stay online when server is started.
|
# Minimum time in seconds to stay online when server is started.
|
||||||
#minimum_online_time = 60
|
#minimum_online_time = 60
|
||||||
|
|
||||||
# Hold client for number of seconds on connect while server starts.
|
[motd]
|
||||||
# 0 to disable and disconnect immediately, keep below Minecraft timeout of 30 seconds.
|
# MOTD, shown in server browser.
|
||||||
#hold_client_for = 25
|
#sleeping = "☠ Server is sleeping\n§2☻ Join to start it up"
|
||||||
|
#starting = "§2☻ Server is starting...\n§7⌛ Please wait..."
|
||||||
# Server start/stop timeout in seconds. Force kill server process if it takes too long.
|
#stopping = "☠ Server going to sleep...\n⌛ Please wait..."
|
||||||
#start_timeout = 300
|
|
||||||
#stop_timeout = 150
|
|
||||||
|
|
||||||
[messages]
|
|
||||||
# MOTDs, shown in server browser.
|
|
||||||
#motd_sleeping = "☠ Server is sleeping\n§2☻ Join to start it up"
|
|
||||||
#motd_starting = "§2☻ Server is starting...\n§7⌛ Please wait..."
|
|
||||||
#motd_stopping = "☠ Server going to sleep...\n⌛ Please wait..."
|
|
||||||
|
|
||||||
# Use MOTD from Minecraft server once known.
|
# Use MOTD from Minecraft server once known.
|
||||||
#use_server_motd = false
|
#from_server = false
|
||||||
|
|
||||||
# Login messages, when user tries to connect.
|
[join]
|
||||||
#login_starting = "Server is starting... §c♥§r\n\nThis may take some time.\n\nPlease try to reconnect in a minute."
|
# Methods to use to occupy a client on join while the server is starting.
|
||||||
#login_stopping = "Server is going to sleep... §7☠§r\n\nPlease try to reconnect in a minute to wake it again."
|
# Read about all methods and configure them below.
|
||||||
|
# Methods are used in order, if none is set, the client disconnects without a message.
|
||||||
|
#methods = [
|
||||||
|
# "hold",
|
||||||
|
# "kick",
|
||||||
|
#]
|
||||||
|
|
||||||
|
[join.kick]
|
||||||
|
# Kick occupation method.
|
||||||
|
# Instantly kicks a client with a message.
|
||||||
|
|
||||||
|
# Message shown when client is kicked while server is starting/stopping.
|
||||||
|
#starting = "Server is starting... §c♥§r\n\nThis may take some time.\n\nPlease try to reconnect in a minute."
|
||||||
|
#stopping = "Server is going to sleep... §7☠§r\n\nPlease try to reconnect in a minute to wake it again."
|
||||||
|
|
||||||
|
[join.hold]
|
||||||
|
# Hold occupation method.
|
||||||
|
# Holds back a joining client while the server is started until it is ready.
|
||||||
|
# 'Connecting the server...' is shown on the client while it's held back.
|
||||||
|
# If the server starts fast enough, the client won't notice it was sleeping at all.
|
||||||
|
# This works for a limited time of 30 seconds, after which the Minecraft client times out.
|
||||||
|
|
||||||
|
# Hold client for number of seconds on connect while server starts.
|
||||||
|
# Keep below Minecraft timeout of 30 seconds.
|
||||||
|
#timeout = 25
|
||||||
|
|
||||||
|
[join.forward]
|
||||||
|
# Forward occupation method.
|
||||||
|
# Instantly forwards (proxies) the client to a different address.
|
||||||
|
# You may need to configure target server for it, such as allowing proxies.
|
||||||
|
# Consumes client, not allowing other join methods afterwards.
|
||||||
|
|
||||||
|
# IP and port to forward to.
|
||||||
|
# The target server will receive original client handshake and login request as received by lazymc.
|
||||||
|
#address = "127.0.0.1:25565"
|
||||||
|
|
||||||
|
# Add HAProxy v2 header to forwarded connections.
|
||||||
|
# See: https://git.io/J1bYb
|
||||||
|
#send_proxy_v2 = false
|
||||||
|
|
||||||
|
[join.lobby]
|
||||||
|
# Lobby occupation method.
|
||||||
|
# The client joins a fake lobby server with an empty world, floating in space.
|
||||||
|
# A message is overlayed on screen to notify the server is starting.
|
||||||
|
# The client will be teleported to the real server once it is ready.
|
||||||
|
# This may keep the client occupied forever if no timeout is set.
|
||||||
|
# Consumes client, not allowing other join methods afterwards.
|
||||||
|
# See: https://git.io/JMIi4
|
||||||
|
|
||||||
|
# !!! WARNING !!!
|
||||||
|
# This is highly experimental, incomplete and unstable.
|
||||||
|
# This may break the game and crash clients.
|
||||||
|
# Don't enable this unless you know what you're doing.
|
||||||
|
#
|
||||||
|
# - Server must be in offline mode
|
||||||
|
# - Server must use Minecraft version 1.16.3 to 1.17.1 (tested with 1.17.1)
|
||||||
|
# - Server must use vanilla Minecraft
|
||||||
|
# - May work with Forge, enable in config, depends on used mods, test before use
|
||||||
|
# - Does not work with other mods, such as FTB
|
||||||
|
|
||||||
|
# Maximum time in seconds in the lobby while the server starts.
|
||||||
|
#timeout = 600
|
||||||
|
|
||||||
|
# Message banner in lobby shown to client.
|
||||||
|
#message = "§2Server is starting\n§7⌛ Please wait..."
|
||||||
|
|
||||||
|
# Sound effect to play when server is ready.
|
||||||
|
#ready_sound = "block.note_block.chime"
|
||||||
|
|
||||||
|
[lockout]
|
||||||
|
# Enable to prevent everybody from connecting through lazymc. Instantly kicks player.
|
||||||
|
#enabled = false
|
||||||
|
|
||||||
|
# Kick players with following message.
|
||||||
|
#message = "Server is closed §7☠§r\n\nPlease try to reconnect in a minute."
|
||||||
|
|
||||||
[rcon]
|
[rcon]
|
||||||
# Enable sleeping server through RCON.
|
# Enable sleeping server through RCON.
|
||||||
# Must be enabled on Windows.
|
# Must be enabled on Windows.
|
||||||
#enabled = true
|
#enabled = false # default: false, true on Windows
|
||||||
|
|
||||||
# Server RCON port. Must differ from public and server port.
|
# Server RCON port. Must differ from public and server port.
|
||||||
#port = 25575
|
#port = 25575
|
||||||
@@ -80,6 +176,15 @@ command = "java -Xmx1G -Xms1G -jar server.jar --nogui"
|
|||||||
#password = ""
|
#password = ""
|
||||||
#randomize_password = true
|
#randomize_password = true
|
||||||
|
|
||||||
|
# Add HAProxy v2 header to RCON connections.
|
||||||
|
# See: https://git.io/J1bYb
|
||||||
|
#send_proxy_v2 = false
|
||||||
|
|
||||||
[advanced]
|
[advanced]
|
||||||
# Automatically update values in Minecraft server.properties file as required.
|
# Automatically update values in Minecraft server.properties file as required.
|
||||||
#rewrite_server_properties = true
|
#rewrite_server_properties = true
|
||||||
|
|
||||||
|
[config]
|
||||||
|
# lazymc version this configuration is for.
|
||||||
|
# Don't change unless you know what you're doing.
|
||||||
|
version = "0.2.10"
|
||||||
|
BIN
res/screenshot/lobby.png
Normal file
BIN
res/screenshot/lobby.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 157 KiB |
@@ -1,5 +1,7 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
|
# See: https://git.io/JMIKH
|
||||||
|
|
||||||
# Server JAR file, set this to your own
|
# Server JAR file, set this to your own
|
||||||
FILE=server.jar
|
FILE=server.jar
|
||||||
|
|
||||||
|
BIN
res/unknown_server.png
Normal file
BIN
res/unknown_server.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 11 KiB |
BIN
res/unknown_server_optimized.png
Normal file
BIN
res/unknown_server_optimized.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 6.8 KiB |
@@ -9,7 +9,7 @@ use crate::util::error::{quit, quit_error, ErrorHintsBuilder};
|
|||||||
/// Invoke config test command.
|
/// Invoke config test command.
|
||||||
pub fn invoke(matches: &ArgMatches) {
|
pub fn invoke(matches: &ArgMatches) {
|
||||||
// Get config path, attempt to canonicalize
|
// Get config path, attempt to canonicalize
|
||||||
let mut path = PathBuf::from(matches.value_of("config").unwrap());
|
let mut path = PathBuf::from(matches.get_one::<String>("config").unwrap());
|
||||||
if let Ok(p) = path.canonicalize() {
|
if let Ok(p) = path.canonicalize() {
|
||||||
path = p;
|
path = p;
|
||||||
}
|
}
|
||||||
|
@@ -8,7 +8,7 @@ use crate::util::error::{quit_error, quit_error_msg, ErrorHintsBuilder};
|
|||||||
/// Invoke config test command.
|
/// Invoke config test command.
|
||||||
pub fn invoke(matches: &ArgMatches) {
|
pub fn invoke(matches: &ArgMatches) {
|
||||||
// Get config path, attempt to canonicalize
|
// Get config path, attempt to canonicalize
|
||||||
let mut path = PathBuf::from(matches.value_of("config").unwrap());
|
let mut path = PathBuf::from(matches.get_one::<String>("config").unwrap());
|
||||||
if let Ok(p) = path.canonicalize() {
|
if let Ok(p) = path.canonicalize() {
|
||||||
path = p;
|
path = p;
|
||||||
}
|
}
|
||||||
|
@@ -3,8 +3,9 @@ use std::sync::Arc;
|
|||||||
|
|
||||||
use clap::ArgMatches;
|
use clap::ArgMatches;
|
||||||
|
|
||||||
use crate::config::{self, Config};
|
use crate::config::{self, Config, Server as ConfigServer};
|
||||||
use crate::mc::server_properties;
|
use crate::mc::server_properties;
|
||||||
|
use crate::proto;
|
||||||
use crate::service;
|
use crate::service;
|
||||||
|
|
||||||
/// RCON randomized password length.
|
/// RCON randomized password length.
|
||||||
@@ -119,7 +120,7 @@ fn rewrite_server_properties(config: &Config) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Ensure server directory is set, it must exist
|
// Ensure server directory is set, it must exist
|
||||||
let dir = match &config.server.directory {
|
let dir = match ConfigServer::server_directory(config) {
|
||||||
Some(dir) => dir,
|
Some(dir) => dir,
|
||||||
None => {
|
None => {
|
||||||
warn!(target: "lazymc", "Not rewriting {} file, server directory not configured (server.directory)", server_properties::FILE);
|
warn!(target: "lazymc", "Not rewriting {} file, server directory not configured (server.directory)", server_properties::FILE);
|
||||||
@@ -141,6 +142,14 @@ fn rewrite_server_properties(config: &Config) {
|
|||||||
changes.extend([("prevent-proxy-connections", "false".into())]);
|
changes.extend([("prevent-proxy-connections", "false".into())]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update network compression threshold for lobby mode
|
||||||
|
if config.join.methods.contains(&config::Method::Lobby) {
|
||||||
|
changes.extend([(
|
||||||
|
"network-compression-threshold",
|
||||||
|
proto::COMPRESSION_THRESHOLD.to_string(),
|
||||||
|
)]);
|
||||||
|
}
|
||||||
|
|
||||||
// Add RCON configuration
|
// Add RCON configuration
|
||||||
#[cfg(feature = "rcon")]
|
#[cfg(feature = "rcon")]
|
||||||
if config.rcon.enabled {
|
if config.rcon.enabled {
|
||||||
|
25
src/cli.rs
25
src/cli.rs
@@ -1,23 +1,28 @@
|
|||||||
use clap::{App, AppSettings, Arg};
|
use clap::{Arg, Command};
|
||||||
|
|
||||||
/// The clap app for CLI argument parsing.
|
/// The clap app for CLI argument parsing.
|
||||||
pub fn app() -> App<'static> {
|
pub fn app() -> Command {
|
||||||
App::new(crate_name!())
|
Command::new(crate_name!())
|
||||||
.version(crate_version!())
|
.version(crate_version!())
|
||||||
.author(crate_authors!())
|
.author(crate_authors!())
|
||||||
.about(crate_description!())
|
.about(crate_description!())
|
||||||
.subcommand(
|
.subcommand(
|
||||||
App::new("start")
|
Command::new("start")
|
||||||
.alias("run")
|
.alias("run")
|
||||||
.about("Start lazymc and server (default)"),
|
.about("Start lazymc and server (default)"),
|
||||||
)
|
)
|
||||||
.subcommand(
|
.subcommand(
|
||||||
App::new("config")
|
Command::new("config")
|
||||||
.alias("cfg")
|
.alias("cfg")
|
||||||
.about("Config actions")
|
.about("Config actions")
|
||||||
.setting(AppSettings::SubcommandRequiredElseHelp)
|
.arg_required_else_help(true)
|
||||||
.subcommand(App::new("generate").alias("gen").about("Generate config"))
|
.subcommand_required(true)
|
||||||
.subcommand(App::new("test").about("Test config")),
|
.subcommand(
|
||||||
|
Command::new("generate")
|
||||||
|
.alias("gen")
|
||||||
|
.about("Generate config"),
|
||||||
|
)
|
||||||
|
.subcommand(Command::new("test").about("Test config")),
|
||||||
)
|
)
|
||||||
.arg(
|
.arg(
|
||||||
Arg::new("config")
|
Arg::new("config")
|
||||||
@@ -27,7 +32,7 @@ pub fn app() -> App<'static> {
|
|||||||
.global(true)
|
.global(true)
|
||||||
.value_name("FILE")
|
.value_name("FILE")
|
||||||
.default_value(crate::config::CONFIG_FILE)
|
.default_value(crate::config::CONFIG_FILE)
|
||||||
.about("Use config file")
|
.help("Use config file")
|
||||||
.takes_value(true),
|
.num_args(1),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
342
src/config.rs
342
src/config.rs
@@ -1,23 +1,28 @@
|
|||||||
use std::fs;
|
use std::fs;
|
||||||
use std::io;
|
use std::io;
|
||||||
use std::net::SocketAddr;
|
use std::net::SocketAddr;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use clap::ArgMatches;
|
use clap::ArgMatches;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
use version_compare::Cmp;
|
||||||
|
|
||||||
use crate::proto;
|
use crate::proto;
|
||||||
use crate::util::error::{quit_error, quit_error_msg, ErrorHintsBuilder};
|
use crate::util::error::{quit_error, quit_error_msg, ErrorHintsBuilder};
|
||||||
|
use crate::util::serde::to_socket_addrs;
|
||||||
|
|
||||||
/// Default configuration file location.
|
/// Default configuration file location.
|
||||||
pub const CONFIG_FILE: &str = "lazymc.toml";
|
pub const CONFIG_FILE: &str = "lazymc.toml";
|
||||||
|
|
||||||
|
/// Configuration version user should be using, or warning will be shown.
|
||||||
|
const CONFIG_VERSION: &str = "0.2.8";
|
||||||
|
|
||||||
/// Load config from file, based on CLI arguments.
|
/// Load config from file, based on CLI arguments.
|
||||||
///
|
///
|
||||||
/// Quits with an error message on failure.
|
/// Quits with an error message on failure.
|
||||||
pub fn load(matches: &ArgMatches) -> Config {
|
pub fn load(matches: &ArgMatches) -> Config {
|
||||||
// Get config path, attempt to canonicalize
|
// Get config path, attempt to canonicalize
|
||||||
let mut path = PathBuf::from(matches.value_of("config").unwrap());
|
let mut path = PathBuf::from(matches.get_one::<String>("config").unwrap());
|
||||||
if let Ok(p) = path.canonicalize() {
|
if let Ok(p) = path.canonicalize() {
|
||||||
path = p;
|
path = p;
|
||||||
}
|
}
|
||||||
@@ -58,6 +63,12 @@ pub fn load(matches: &ArgMatches) -> Config {
|
|||||||
/// Configuration.
|
/// Configuration.
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
|
/// Configuration path if known.
|
||||||
|
///
|
||||||
|
/// Should be used as base directory for filesystem operations.
|
||||||
|
#[serde(skip)]
|
||||||
|
pub path: Option<PathBuf>,
|
||||||
|
|
||||||
/// Public configuration.
|
/// Public configuration.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub public: Public,
|
pub public: Public,
|
||||||
@@ -69,9 +80,17 @@ pub struct Config {
|
|||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub time: Time,
|
pub time: Time,
|
||||||
|
|
||||||
/// Messages, shown to the user.
|
/// MOTD configuration.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub messages: Messages,
|
pub motd: Motd,
|
||||||
|
|
||||||
|
/// Join configuration.
|
||||||
|
#[serde(default)]
|
||||||
|
pub join: Join,
|
||||||
|
|
||||||
|
/// Lockout feature.
|
||||||
|
#[serde(default)]
|
||||||
|
pub lockout: Lockout,
|
||||||
|
|
||||||
/// RCON configuration.
|
/// RCON configuration.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
@@ -80,13 +99,33 @@ pub struct Config {
|
|||||||
/// Advanced configuration.
|
/// Advanced configuration.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub advanced: Advanced,
|
pub advanced: Advanced,
|
||||||
|
|
||||||
|
/// Config configuration.
|
||||||
|
#[serde(default)]
|
||||||
|
pub config: ConfigConfig,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Config {
|
impl Config {
|
||||||
/// Load configuration from file.
|
/// Load configuration from file.
|
||||||
pub fn load<P: AsRef<Path>>(path: P) -> Result<Self, io::Error> {
|
pub fn load(path: PathBuf) -> Result<Self, io::Error> {
|
||||||
let data = fs::read(path)?;
|
let data = fs::read(&path)?;
|
||||||
let config = toml::from_slice(&data)?;
|
let mut config: Config = toml::from_slice(&data)?;
|
||||||
|
|
||||||
|
// Show warning if config version is problematic
|
||||||
|
match &config.config.version {
|
||||||
|
None => warn!(target: "lazymc::config", "Config version unknown, it may be outdated"),
|
||||||
|
Some(version) => match version_compare::compare_to(version, CONFIG_VERSION, Cmp::Ge) {
|
||||||
|
Ok(false) => {
|
||||||
|
warn!(target: "lazymc::config", "Config is for older lazymc version, you may need to update it")
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
warn!(target: "lazymc::config", "Config version is invalid, you may need to update it")
|
||||||
|
}
|
||||||
|
Ok(true) => {}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
config.path.replace(path);
|
||||||
|
|
||||||
Ok(config)
|
Ok(config)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -96,6 +135,7 @@ impl Config {
|
|||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub struct Public {
|
pub struct Public {
|
||||||
/// Public address.
|
/// Public address.
|
||||||
|
#[serde(deserialize_with = "to_socket_addrs")]
|
||||||
pub address: SocketAddr,
|
pub address: SocketAddr,
|
||||||
|
|
||||||
/// Minecraft protocol version name hint.
|
/// Minecraft protocol version name hint.
|
||||||
@@ -119,16 +159,26 @@ impl Default for Public {
|
|||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
pub struct Server {
|
pub struct Server {
|
||||||
/// Server directory.
|
/// Server directory.
|
||||||
|
///
|
||||||
|
/// Private because you should use `Server::server_directory()` instead.
|
||||||
#[serde(default = "option_pathbuf_dot")]
|
#[serde(default = "option_pathbuf_dot")]
|
||||||
pub directory: Option<PathBuf>,
|
directory: Option<PathBuf>,
|
||||||
|
|
||||||
/// Start command.
|
/// Start command.
|
||||||
pub command: String,
|
pub command: String,
|
||||||
|
|
||||||
/// Server address.
|
/// Server address.
|
||||||
#[serde(default = "server_address_default")]
|
#[serde(
|
||||||
|
deserialize_with = "to_socket_addrs",
|
||||||
|
default = "server_address_default"
|
||||||
|
)]
|
||||||
pub address: SocketAddr,
|
pub address: SocketAddr,
|
||||||
|
|
||||||
|
/// Freeze the server process instead of restarting it when no players online, making it start up faster.
|
||||||
|
/// Only works on Unix (Linux or MacOS)
|
||||||
|
#[serde(default = "bool_true")]
|
||||||
|
pub freeze_process: bool,
|
||||||
|
|
||||||
/// Immediately wake server when starting lazymc.
|
/// Immediately wake server when starting lazymc.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub wake_on_start: bool,
|
pub wake_on_start: bool,
|
||||||
@@ -136,6 +186,51 @@ pub struct Server {
|
|||||||
/// Immediately wake server after crash.
|
/// Immediately wake server after crash.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub wake_on_crash: bool,
|
pub wake_on_crash: bool,
|
||||||
|
|
||||||
|
/// Probe required server details when starting lazymc, wakes server on start.
|
||||||
|
#[serde(default)]
|
||||||
|
pub probe_on_start: bool,
|
||||||
|
|
||||||
|
/// Whether this server runs forge.
|
||||||
|
#[serde(default)]
|
||||||
|
pub forge: bool,
|
||||||
|
|
||||||
|
/// Server starting timeout. Force kill server process if it takes longer.
|
||||||
|
#[serde(default = "u32_300")]
|
||||||
|
pub start_timeout: u32,
|
||||||
|
|
||||||
|
/// Server stopping timeout. Force kill server process if it takes longer.
|
||||||
|
#[serde(default = "u32_150")]
|
||||||
|
pub stop_timeout: u32,
|
||||||
|
|
||||||
|
/// To wake server, user must be in server whitelist if enabled on server.
|
||||||
|
#[serde(default = "bool_true")]
|
||||||
|
pub wake_whitelist: bool,
|
||||||
|
|
||||||
|
/// 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,
|
||||||
|
|
||||||
|
/// Add HAProxy v2 header to proxied connections.
|
||||||
|
#[serde(default)]
|
||||||
|
pub send_proxy_v2: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Server {
|
||||||
|
/// Get the server directory.
|
||||||
|
///
|
||||||
|
/// This does not check whether it exists.
|
||||||
|
pub fn server_directory(config: &Config) -> Option<PathBuf> {
|
||||||
|
// Get directory, relative to config directory if known
|
||||||
|
match config.path.as_ref().and_then(|p| p.parent()) {
|
||||||
|
Some(config_dir) => Some(config_dir.join(config.server.directory.as_ref()?)),
|
||||||
|
None => config.server.directory.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Time configuration.
|
/// Time configuration.
|
||||||
@@ -148,24 +243,6 @@ pub struct Time {
|
|||||||
/// Minimum time in seconds to stay online when server is started.
|
/// Minimum time in seconds to stay online when server is started.
|
||||||
#[serde(default, alias = "minimum_online_time")]
|
#[serde(default, alias = "minimum_online_time")]
|
||||||
pub min_online_time: u32,
|
pub min_online_time: u32,
|
||||||
|
|
||||||
/// Hold client for number of seconds while server starts, instead of kicking immediately.
|
|
||||||
pub hold_client_for: u32,
|
|
||||||
|
|
||||||
/// Server starting timeout. Force kill server process if it takes longer.
|
|
||||||
#[serde(alias = "starting_timeout")]
|
|
||||||
pub start_timeout: u32,
|
|
||||||
|
|
||||||
/// Server stopping timeout. Force kill server process if it takes longer.
|
|
||||||
#[serde(alias = "stopping_timeout")]
|
|
||||||
pub stop_timeout: u32,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Time {
|
|
||||||
/// Whether to hold clients.
|
|
||||||
pub fn hold(&self) -> bool {
|
|
||||||
self.hold_client_for > 0
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Time {
|
impl Default for Time {
|
||||||
@@ -173,45 +250,186 @@ impl Default for Time {
|
|||||||
Self {
|
Self {
|
||||||
sleep_after: 60,
|
sleep_after: 60,
|
||||||
min_online_time: 60,
|
min_online_time: 60,
|
||||||
hold_client_for: 25,
|
|
||||||
start_timeout: 300,
|
|
||||||
stop_timeout: 150,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Message configuration.
|
/// MOTD configuration.
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub struct Messages {
|
pub struct Motd {
|
||||||
/// MOTD when server is sleeping.
|
/// MOTD when server is sleeping.
|
||||||
pub motd_sleeping: String,
|
pub sleeping: String,
|
||||||
|
|
||||||
/// MOTD when server is starting.
|
/// MOTD when server is starting.
|
||||||
pub motd_starting: String,
|
pub starting: String,
|
||||||
|
|
||||||
/// MOTD when server is stopping.
|
/// MOTD when server is stopping.
|
||||||
pub motd_stopping: String,
|
pub stopping: String,
|
||||||
|
|
||||||
/// Use MOTD from Minecraft server once known.
|
/// Use MOTD from Minecraft server once known.
|
||||||
pub use_server_motd: bool,
|
pub from_server: bool,
|
||||||
|
|
||||||
/// Login message when server is starting.
|
|
||||||
pub login_starting: String,
|
|
||||||
|
|
||||||
/// Login message when server is stopping.
|
|
||||||
pub login_stopping: String,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Messages {
|
impl Default for Motd {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
motd_sleeping: "☠ Server is sleeping\n§2☻ Join to start it up".into(),
|
sleeping: "☠ Server is sleeping\n§2☻ Join to start it up".into(),
|
||||||
motd_starting: "§2☻ Server is starting...\n§7⌛ Please wait...".into(),
|
starting: "§2☻ Server is starting...\n§7⌛ Please wait...".into(),
|
||||||
motd_stopping: "☠ Server going to sleep...\n⌛ Please wait...".into(),
|
stopping: "☠ Server going to sleep...\n⌛ Please wait...".into(),
|
||||||
use_server_motd: false,
|
from_server: false,
|
||||||
login_starting: "Server is starting... §c♥§r\n\nThis may take some time.\n\nPlease try to reconnect in a minute.".into(),
|
}
|
||||||
login_stopping: "Server is going to sleep... §7☠§r\n\nPlease try to reconnect in a minute to wake it again.".into(),
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Join method types.
|
||||||
|
#[derive(Debug, Deserialize, Copy, Clone, Eq, PartialEq)]
|
||||||
|
#[serde(rename_all = "lowercase")]
|
||||||
|
pub enum Method {
|
||||||
|
/// Kick client with message.
|
||||||
|
Kick,
|
||||||
|
|
||||||
|
/// Hold client connection until server is ready.
|
||||||
|
Hold,
|
||||||
|
|
||||||
|
/// Forward connection to another host.
|
||||||
|
Forward,
|
||||||
|
|
||||||
|
/// Keep client in temporary fake lobby until server is ready.
|
||||||
|
Lobby,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Join configuration.
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
#[serde(default)]
|
||||||
|
pub struct Join {
|
||||||
|
/// Join methods.
|
||||||
|
pub methods: Vec<Method>,
|
||||||
|
|
||||||
|
/// Join kick configuration.
|
||||||
|
#[serde(default)]
|
||||||
|
pub kick: JoinKick,
|
||||||
|
|
||||||
|
/// Join hold configuration.
|
||||||
|
#[serde(default)]
|
||||||
|
pub hold: JoinHold,
|
||||||
|
|
||||||
|
/// Join forward configuration.
|
||||||
|
#[serde(default)]
|
||||||
|
pub forward: JoinForward,
|
||||||
|
|
||||||
|
/// Join lobby configuration.
|
||||||
|
#[serde(default)]
|
||||||
|
pub lobby: JoinLobby,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Join {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
methods: vec![Method::Hold, Method::Kick],
|
||||||
|
kick: Default::default(),
|
||||||
|
hold: Default::default(),
|
||||||
|
forward: Default::default(),
|
||||||
|
lobby: Default::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Join kick configuration.
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
#[serde(default)]
|
||||||
|
pub struct JoinKick {
|
||||||
|
/// Kick message when server is starting.
|
||||||
|
pub starting: String,
|
||||||
|
|
||||||
|
/// Kick message when server is stopping.
|
||||||
|
pub stopping: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for JoinKick {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
starting: "Server is starting... §c♥§r\n\nThis may take some time.\n\nPlease try to reconnect in a minute.".into(),
|
||||||
|
stopping: "Server is going to sleep... §7☠§r\n\nPlease try to reconnect in a minute to wake it again.".into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Join hold configuration.
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
#[serde(default)]
|
||||||
|
pub struct JoinHold {
|
||||||
|
/// Hold client for number of seconds on connect while server starts.
|
||||||
|
pub timeout: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for JoinHold {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self { timeout: 25 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Join forward configuration.
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
#[serde(default)]
|
||||||
|
pub struct JoinForward {
|
||||||
|
/// IP and port to forward to.
|
||||||
|
#[serde(deserialize_with = "to_socket_addrs")]
|
||||||
|
pub address: SocketAddr,
|
||||||
|
|
||||||
|
/// Add HAProxy v2 header to proxied connections.
|
||||||
|
#[serde(default)]
|
||||||
|
pub send_proxy_v2: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for JoinForward {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
address: "127.0.0.1:25565".parse().unwrap(),
|
||||||
|
send_proxy_v2: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// Join lobby configuration.
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
#[serde(default)]
|
||||||
|
pub struct JoinLobby {
|
||||||
|
/// Hold client in lobby for number of seconds on connect while server starts.
|
||||||
|
pub timeout: u32,
|
||||||
|
|
||||||
|
/// Message banner in lobby shown to client.
|
||||||
|
pub message: String,
|
||||||
|
|
||||||
|
/// Sound effect to play when server is ready.
|
||||||
|
pub ready_sound: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for JoinLobby {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
timeout: 10 * 60,
|
||||||
|
message: "§2Server is starting\n§7⌛ Please wait...".into(),
|
||||||
|
ready_sound: Some("block.note_block.chime".into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Lockout configuration.
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
#[serde(default)]
|
||||||
|
pub struct Lockout {
|
||||||
|
/// Enable to prevent everybody from connecting through lazymc. Instantly kicks player.
|
||||||
|
pub enabled: bool,
|
||||||
|
|
||||||
|
/// Kick players with following message.
|
||||||
|
pub message: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Lockout {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
enabled: false,
|
||||||
|
message: "Server is closed §7☠§r\n\nPlease come back another time.".into(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -231,15 +449,19 @@ pub struct Rcon {
|
|||||||
|
|
||||||
/// Randomize server RCON password on each start.
|
/// Randomize server RCON password on each start.
|
||||||
pub randomize_password: bool,
|
pub randomize_password: bool,
|
||||||
|
|
||||||
|
/// Add HAProxy v2 header to RCON connections.
|
||||||
|
pub send_proxy_v2: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Rcon {
|
impl Default for Rcon {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
enabled: true,
|
enabled: cfg!(windows),
|
||||||
port: 25575,
|
port: 25575,
|
||||||
password: "".into(),
|
password: "".into(),
|
||||||
randomize_password: true,
|
randomize_password: true,
|
||||||
|
send_proxy_v2: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -260,6 +482,14 @@ impl Default for Advanced {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Config configuration.
|
||||||
|
#[derive(Debug, Deserialize, Default)]
|
||||||
|
#[serde(default)]
|
||||||
|
pub struct ConfigConfig {
|
||||||
|
/// Configuration for lazymc version.
|
||||||
|
pub version: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
fn option_pathbuf_dot() -> Option<PathBuf> {
|
fn option_pathbuf_dot() -> Option<PathBuf> {
|
||||||
Some(".".into())
|
Some(".".into())
|
||||||
}
|
}
|
||||||
@@ -267,3 +497,15 @@ fn option_pathbuf_dot() -> Option<PathBuf> {
|
|||||||
fn server_address_default() -> SocketAddr {
|
fn server_address_default() -> SocketAddr {
|
||||||
"127.0.0.1:25566".parse().unwrap()
|
"127.0.0.1:25566".parse().unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn u32_300() -> u32 {
|
||||||
|
300
|
||||||
|
}
|
||||||
|
|
||||||
|
fn u32_150() -> u32 {
|
||||||
|
300
|
||||||
|
}
|
||||||
|
|
||||||
|
fn bool_true() -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
255
src/forge.rs
Normal file
255
src/forge.rs
Normal file
@@ -0,0 +1,255 @@
|
|||||||
|
#[cfg(feature = "lobby")]
|
||||||
|
use std::sync::Arc;
|
||||||
|
#[cfg(feature = "lobby")]
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
#[cfg(feature = "lobby")]
|
||||||
|
use bytes::BytesMut;
|
||||||
|
use minecraft_protocol::decoder::Decoder;
|
||||||
|
use minecraft_protocol::encoder::Encoder;
|
||||||
|
use minecraft_protocol::version::forge_v1_13::login::{Acknowledgement, LoginWrapper, ModList};
|
||||||
|
use minecraft_protocol::version::v1_14_4::login::{LoginPluginRequest, LoginPluginResponse};
|
||||||
|
use minecraft_protocol::version::PacketId;
|
||||||
|
#[cfg(feature = "lobby")]
|
||||||
|
use tokio::io::AsyncWriteExt;
|
||||||
|
use tokio::net::tcp::WriteHalf;
|
||||||
|
#[cfg(feature = "lobby")]
|
||||||
|
use tokio::net::TcpStream;
|
||||||
|
#[cfg(feature = "lobby")]
|
||||||
|
use tokio::time;
|
||||||
|
|
||||||
|
use crate::forge;
|
||||||
|
use crate::proto::client::Client;
|
||||||
|
#[cfg(feature = "lobby")]
|
||||||
|
use crate::proto::client::ClientState;
|
||||||
|
use crate::proto::packet;
|
||||||
|
use crate::proto::packet::RawPacket;
|
||||||
|
#[cfg(feature = "lobby")]
|
||||||
|
use crate::proto::packets;
|
||||||
|
#[cfg(feature = "lobby")]
|
||||||
|
use crate::server::Server;
|
||||||
|
|
||||||
|
/// Forge status magic.
|
||||||
|
pub const STATUS_MAGIC: &str = "\0FML2\0";
|
||||||
|
|
||||||
|
/// Forge plugin wrapper login plugin request channel.
|
||||||
|
pub const CHANNEL_LOGIN_WRAPPER: &str = "fml:loginwrapper";
|
||||||
|
|
||||||
|
/// Forge handshake channel.
|
||||||
|
pub const CHANNEL_HANDSHAKE: &str = "fml:handshake";
|
||||||
|
|
||||||
|
/// Timeout for draining Forge plugin responses from client.
|
||||||
|
#[cfg(feature = "lobby")]
|
||||||
|
const CLIENT_DRAIN_FORGE_TIMEOUT: Duration = Duration::from_secs(5);
|
||||||
|
|
||||||
|
/// Respond with Forge login wrapper packet.
|
||||||
|
pub async fn respond_forge_login_packet(
|
||||||
|
client: &Client,
|
||||||
|
writer: &mut WriteHalf<'_>,
|
||||||
|
message_id: i32,
|
||||||
|
forge_channel: String,
|
||||||
|
forge_packet: impl PacketId + Encoder,
|
||||||
|
) -> Result<(), ()> {
|
||||||
|
// Encode Forge packet to data
|
||||||
|
let mut forge_data = Vec::new();
|
||||||
|
forge_packet.encode(&mut forge_data).map_err(|_| ())?;
|
||||||
|
|
||||||
|
// Encode Forge payload
|
||||||
|
let forge_payload =
|
||||||
|
RawPacket::new(forge_packet.packet_id(), forge_data).encode_without_len(client)?;
|
||||||
|
|
||||||
|
// Wrap Forge payload in login wrapper
|
||||||
|
let mut payload = Vec::new();
|
||||||
|
let packet = LoginWrapper {
|
||||||
|
channel: forge_channel,
|
||||||
|
packet: forge_payload,
|
||||||
|
};
|
||||||
|
packet.encode(&mut payload).map_err(|_| ())?;
|
||||||
|
|
||||||
|
// Write login plugin request with forge payload
|
||||||
|
packet::write_packet(
|
||||||
|
LoginPluginResponse {
|
||||||
|
message_id,
|
||||||
|
successful: true,
|
||||||
|
data: payload,
|
||||||
|
},
|
||||||
|
client,
|
||||||
|
writer,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Respond to a Forge login plugin request.
|
||||||
|
pub async fn respond_login_plugin_request(
|
||||||
|
client: &Client,
|
||||||
|
packet: LoginPluginRequest,
|
||||||
|
writer: &mut WriteHalf<'_>,
|
||||||
|
) -> Result<(), ()> {
|
||||||
|
// Decode Forge login wrapper packet
|
||||||
|
let (message_id, login_wrapper, packet) =
|
||||||
|
forge::decode_forge_login_packet(client, packet).await?;
|
||||||
|
|
||||||
|
// Determine whether we received the mod list
|
||||||
|
let is_unknown_header = login_wrapper.channel != forge::CHANNEL_HANDSHAKE;
|
||||||
|
let is_mod_list = !is_unknown_header && packet.id == ModList::PACKET_ID;
|
||||||
|
|
||||||
|
// If not the mod list, just acknowledge
|
||||||
|
if !is_mod_list {
|
||||||
|
trace!(target: "lazymc::forge", "Acknowledging login plugin request");
|
||||||
|
forge::respond_forge_login_packet(
|
||||||
|
client,
|
||||||
|
writer,
|
||||||
|
message_id,
|
||||||
|
login_wrapper.channel,
|
||||||
|
Acknowledgement {},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(|_| {
|
||||||
|
error!(target: "lazymc::forge", "Failed to send Forge login plugin request acknowledgement");
|
||||||
|
})?;
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
trace!(target: "lazymc::forge", "Sending mod list reply to server with same contents");
|
||||||
|
|
||||||
|
// Parse mod list, transform into reply
|
||||||
|
let mod_list = ModList::decode(&mut packet.data.as_slice()).map_err(|err| {
|
||||||
|
error!(target: "lazymc::forge", "Failed to decode Forge mod list: {:?}", err);
|
||||||
|
})?;
|
||||||
|
let mod_list_reply = mod_list.into_reply();
|
||||||
|
|
||||||
|
// We got mod list, respond with reply
|
||||||
|
forge::respond_forge_login_packet(
|
||||||
|
client,
|
||||||
|
writer,
|
||||||
|
message_id,
|
||||||
|
login_wrapper.channel,
|
||||||
|
mod_list_reply,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(|_| {
|
||||||
|
error!(target: "lazymc::forge", "Failed to send Forge login plugin mod list reply");
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Decode a Forge login wrapper packet from login plugin request.
|
||||||
|
///
|
||||||
|
/// Returns (`message_id`, `login_wrapper`, `packet`).
|
||||||
|
pub async fn decode_forge_login_packet(
|
||||||
|
client: &Client,
|
||||||
|
plugin_request: LoginPluginRequest,
|
||||||
|
) -> Result<(i32, LoginWrapper, RawPacket), ()> {
|
||||||
|
// Validate channel
|
||||||
|
assert_eq!(plugin_request.channel, CHANNEL_LOGIN_WRAPPER);
|
||||||
|
|
||||||
|
// Decode login wrapped packet
|
||||||
|
let login_wrapper =
|
||||||
|
LoginWrapper::decode(&mut plugin_request.data.as_slice()).map_err(|err| {
|
||||||
|
error!(target: "lazymc::forge", "Failed to decode Forge LoginWrapper packet: {:?}", err);
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// Parse packet
|
||||||
|
let packet = RawPacket::decode_without_len(client, &login_wrapper.packet).map_err(|err| {
|
||||||
|
error!(target: "lazymc::forge", "Failed to decode Forge LoginWrapper packet contents: {:?}", err);
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok((plugin_request.message_id, login_wrapper, packet))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Replay the Forge login payload for a client.
|
||||||
|
#[cfg(feature = "lobby")]
|
||||||
|
pub async fn replay_login_payload(
|
||||||
|
client: &Client,
|
||||||
|
inbound: &mut TcpStream,
|
||||||
|
server: Arc<Server>,
|
||||||
|
inbound_buf: &mut BytesMut,
|
||||||
|
) -> Result<(), ()> {
|
||||||
|
debug!(target: "lazymc::lobby", "Replaying Forge login procedure for lobby client...");
|
||||||
|
|
||||||
|
// Replay each Forge packet
|
||||||
|
for packet in server.forge_payload.read().await.as_slice() {
|
||||||
|
inbound.write_all(packet).await.map_err(|err| {
|
||||||
|
error!(target: "lazymc::lobby", "Failed to send Forge join payload to lobby client, will likely cause issues: {}", err);
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Drain all responses
|
||||||
|
let count = server.forge_payload.read().await.len();
|
||||||
|
drain_forge_responses(client, inbound, inbound_buf, count).await?;
|
||||||
|
|
||||||
|
trace!(target: "lazymc::lobby", "Forge join payload replayed");
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Drain Forge login plugin response packets from stream.
|
||||||
|
#[cfg(feature = "lobby")]
|
||||||
|
async fn drain_forge_responses(
|
||||||
|
client: &Client,
|
||||||
|
inbound: &mut TcpStream,
|
||||||
|
buf: &mut BytesMut,
|
||||||
|
mut count: usize,
|
||||||
|
) -> Result<(), ()> {
|
||||||
|
let (mut reader, mut _writer) = inbound.split();
|
||||||
|
|
||||||
|
loop {
|
||||||
|
// We're done if count is zero
|
||||||
|
if count == 0 {
|
||||||
|
trace!(target: "lazymc::forge", "Drained all plugin responses from client");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read packet from stream with timeout
|
||||||
|
let read_packet_task = packet::read_packet(client, buf, &mut reader);
|
||||||
|
let timeout = time::timeout(CLIENT_DRAIN_FORGE_TIMEOUT, read_packet_task).await;
|
||||||
|
let read_packet_task = match timeout {
|
||||||
|
Ok(result) => result,
|
||||||
|
Err(_) => {
|
||||||
|
error!(target: "lazymc::forge", "Expected more plugin responses from client, but didn't receive anything in a while, may be problematic");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Read packet from stream
|
||||||
|
let (packet, _raw) = match read_packet_task {
|
||||||
|
Ok(Some(packet)) => packet,
|
||||||
|
Ok(None) => break,
|
||||||
|
Err(_) => {
|
||||||
|
error!(target: "lazymc::forge", "Closing connection, error occurred");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Grab client state
|
||||||
|
let client_state = client.state();
|
||||||
|
|
||||||
|
// Catch login plugin resposne
|
||||||
|
if client_state == ClientState::Login
|
||||||
|
&& packet.id == packets::login::SERVER_LOGIN_PLUGIN_RESPONSE
|
||||||
|
{
|
||||||
|
trace!(target: "lazymc::forge", "Voiding plugin response from client");
|
||||||
|
count -= 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: instantly return on this packet?
|
||||||
|
// // Hijack login success
|
||||||
|
// if client_state == ClientState::Login && packet.id == packets::login::CLIENT_LOGIN_SUCCESS {
|
||||||
|
// trace!(target: "lazymc::forge", "Got login success from server connection, change to play mode");
|
||||||
|
|
||||||
|
// // Switch to play state
|
||||||
|
// tmp_client.set_state(ClientState::Play);
|
||||||
|
|
||||||
|
// return Ok(forge_payload);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// Show unhandled packet warning
|
||||||
|
debug!(target: "lazymc::forge", "Got unhandled packet from server in record_forge_response:");
|
||||||
|
debug!(target: "lazymc::forge", "- State: {:?}", client_state);
|
||||||
|
debug!(target: "lazymc::forge", "- Packet ID: 0x{:02X} ({})", packet.id, packet.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(())
|
||||||
|
}
|
32
src/join/forward.rs
Normal file
32
src/join/forward.rs
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use bytes::BytesMut;
|
||||||
|
use tokio::net::TcpStream;
|
||||||
|
|
||||||
|
use crate::config::*;
|
||||||
|
use crate::proxy::ProxyHeader;
|
||||||
|
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,
|
||||||
|
ProxyHeader::Proxy.not_none(config.join.forward.send_proxy_v2),
|
||||||
|
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
101
src/join/hold.rs
Normal 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
33
src/join/kick.rs
Normal 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)
|
||||||
|
}
|
46
src/join/lobby.rs
Normal file
46
src/join/lobby.rs
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
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");
|
||||||
|
|
||||||
|
// Must be ready to lobby
|
||||||
|
if must_still_probe(&config, &server).await {
|
||||||
|
warn!(target: "lazymc", "Client connected but lobby is not ready, using next join method, probing not completed");
|
||||||
|
return Ok(MethodResult::Continue(inbound));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check whether we still have to probe before we can use the lobby.
|
||||||
|
async fn must_still_probe(config: &Config, server: &Server) -> bool {
|
||||||
|
must_probe(config) && server.probed_join_game.read().await.is_none()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check whether we must have probed data.
|
||||||
|
fn must_probe(config: &Config) -> bool {
|
||||||
|
config.server.forge
|
||||||
|
}
|
106
src/join/mod.rs
Normal file
106
src/join/mod.rs
Normal 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(())
|
||||||
|
}
|
664
src/lobby.rs
Normal file
664
src/lobby.rs
Normal file
@@ -0,0 +1,664 @@
|
|||||||
|
use std::io::ErrorKind;
|
||||||
|
use std::ops::Deref;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use bytes::BytesMut;
|
||||||
|
use futures::FutureExt;
|
||||||
|
use minecraft_protocol::decoder::Decoder;
|
||||||
|
use minecraft_protocol::version::v1_14_4::login::{
|
||||||
|
LoginPluginRequest, LoginPluginResponse, LoginStart, LoginSuccess, SetCompression,
|
||||||
|
};
|
||||||
|
use tokio::io::AsyncWriteExt;
|
||||||
|
use tokio::net::tcp::{ReadHalf, WriteHalf};
|
||||||
|
use tokio::net::TcpStream;
|
||||||
|
use tokio::select;
|
||||||
|
use tokio::time;
|
||||||
|
|
||||||
|
use crate::config::*;
|
||||||
|
use crate::forge;
|
||||||
|
use crate::mc::uuid;
|
||||||
|
use crate::net;
|
||||||
|
use crate::proto;
|
||||||
|
use crate::proto::client::{Client, ClientInfo, ClientState};
|
||||||
|
use crate::proto::packets::play::join_game::JoinGameData;
|
||||||
|
use crate::proto::{packet, packets};
|
||||||
|
use crate::proxy;
|
||||||
|
use crate::server::{Server, State};
|
||||||
|
|
||||||
|
/// Interval to send keep-alive packets at.
|
||||||
|
pub const KEEP_ALIVE_INTERVAL: Duration = Duration::from_secs(10);
|
||||||
|
|
||||||
|
/// Timeout for creating new server connection for lobby client.
|
||||||
|
const SERVER_CONNECT_TIMEOUT: Duration = Duration::from_secs(2 * 60);
|
||||||
|
|
||||||
|
/// Timeout for server sending join game packet.
|
||||||
|
///
|
||||||
|
/// When the play state is reached, the server should immeditely respond with a join game packet.
|
||||||
|
/// This defines the maximum timeout for waiting on it.
|
||||||
|
const SERVER_JOIN_GAME_TIMEOUT: Duration = Duration::from_secs(20);
|
||||||
|
|
||||||
|
/// Time to wait before responding to newly connected server.
|
||||||
|
///
|
||||||
|
/// Notchian servers are slow, we must wait a little before sending play packets, because the
|
||||||
|
/// server needs time to transition the client into this state.
|
||||||
|
/// See warning at: <https://wiki.vg/Protocol#Login_Success>
|
||||||
|
const SERVER_WARMUP: Duration = Duration::from_secs(1);
|
||||||
|
|
||||||
|
/// Serve lobby service for given client connection.
|
||||||
|
///
|
||||||
|
/// The client must be in the login state, or this will error.
|
||||||
|
// 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_info: ClientInfo,
|
||||||
|
mut inbound: TcpStream,
|
||||||
|
config: Arc<Config>,
|
||||||
|
server: Arc<Server>,
|
||||||
|
queue: BytesMut,
|
||||||
|
) -> Result<(), ()> {
|
||||||
|
let (mut reader, mut writer) = inbound.split();
|
||||||
|
|
||||||
|
// Client must be in login state
|
||||||
|
if client.state() != ClientState::Login {
|
||||||
|
error!(target: "lazymc::lobby", "Client reached lobby service with invalid state: {:?}", client.state());
|
||||||
|
return Err(());
|
||||||
|
}
|
||||||
|
|
||||||
|
// We must have useful client info
|
||||||
|
if client_info.username.is_none() {
|
||||||
|
error!(target: "lazymc::lobby", "Client username is unknown, closing connection");
|
||||||
|
return Err(());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Incoming buffer
|
||||||
|
let mut inbound_buf = queue;
|
||||||
|
|
||||||
|
loop {
|
||||||
|
// Read packet from stream
|
||||||
|
let (packet, _raw) = match packet::read_packet(client, &mut inbound_buf, &mut reader).await
|
||||||
|
{
|
||||||
|
Ok(Some(packet)) => packet,
|
||||||
|
Ok(None) => break,
|
||||||
|
Err(_) => {
|
||||||
|
error!(target: "lazymc", "Closing connection, error occurred");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Grab client state
|
||||||
|
let client_state = client.state();
|
||||||
|
|
||||||
|
// Hijack 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(|_| ())?;
|
||||||
|
|
||||||
|
debug!(target: "lazymc::lobby", "Login on lobby server (user: {})", login_start.name);
|
||||||
|
|
||||||
|
// Replay Forge payload
|
||||||
|
if config.server.forge {
|
||||||
|
forge::replay_login_payload(client, &mut inbound, server.clone(), &mut inbound_buf)
|
||||||
|
.await?;
|
||||||
|
let (_returned_reader, returned_writer) = inbound.split();
|
||||||
|
writer = returned_writer;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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?;
|
||||||
|
client.set_compression(proto::COMPRESSION_THRESHOLD);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Respond with login success, switch to play state
|
||||||
|
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, &client_info, &mut writer, &server).await?;
|
||||||
|
|
||||||
|
// Wait for server to come online
|
||||||
|
stage_wait(client, &client_info, &server, &config, &mut writer).await?;
|
||||||
|
|
||||||
|
// Start new connection to server
|
||||||
|
let server_client_info = client_info.clone();
|
||||||
|
let (server_client, mut outbound, mut server_buf) =
|
||||||
|
connect_to_server(&server_client_info, &inbound, &config).await?;
|
||||||
|
let (returned_reader, returned_writer) = inbound.split();
|
||||||
|
reader = returned_reader;
|
||||||
|
writer = returned_writer;
|
||||||
|
|
||||||
|
// Grab join game packet from server
|
||||||
|
let join_game_data = wait_for_server_join_game(
|
||||||
|
&server_client,
|
||||||
|
&server_client_info,
|
||||||
|
&mut outbound,
|
||||||
|
&mut server_buf,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// Reset lobby title
|
||||||
|
packets::play::title::send(client, &client_info, &mut writer, "").await?;
|
||||||
|
|
||||||
|
// Play ready sound if configured
|
||||||
|
play_lobby_ready_sound(client, &client_info, &mut writer, &config).await?;
|
||||||
|
|
||||||
|
// Wait a second because Notchian servers are slow
|
||||||
|
// See: https://wiki.vg/Protocol#Login_Success
|
||||||
|
trace!(target: "lazymc::lobby", "Waiting a second before relaying client connection...");
|
||||||
|
time::sleep(SERVER_WARMUP).await;
|
||||||
|
|
||||||
|
// Send respawn packet, initiates teleport to real server world
|
||||||
|
packets::play::respawn::lobby_send(client, &client_info, &mut writer, join_game_data)
|
||||||
|
.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
|
||||||
|
// for some blacklisted ones
|
||||||
|
trace!(target: "lazymc::lobby", "Voiding remaining incoming lobby client data before relay to real server");
|
||||||
|
drain_stream(&mut reader).await?;
|
||||||
|
|
||||||
|
// Client and server connection ready now, move client to proxy
|
||||||
|
debug!(target: "lazymc::lobby", "Server connection ready, relaying lobby client to proxy");
|
||||||
|
route_proxy(inbound, outbound, server_buf);
|
||||||
|
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show unhandled packet warning
|
||||||
|
debug!(target: "lazymc", "Got unhandled packet:");
|
||||||
|
debug!(target: "lazymc", "- State: {:?}", client_state);
|
||||||
|
debug!(target: "lazymc", "- Packet ID: 0x{:02X} ({})", packet.id, packet.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gracefully close connection
|
||||||
|
net::close_tcp_stream(inbound).await.map_err(|_| ())?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Respond to client with a set compression packet.
|
||||||
|
async fn respond_set_compression(
|
||||||
|
client: &Client,
|
||||||
|
writer: &mut WriteHalf<'_>,
|
||||||
|
threshold: i32,
|
||||||
|
) -> Result<(), ()> {
|
||||||
|
packet::write_packet(SetCompression { threshold }, client, writer).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Respond to client with login success packet
|
||||||
|
// TODO: support online mode here
|
||||||
|
async fn respond_login_success(
|
||||||
|
client: &Client,
|
||||||
|
writer: &mut WriteHalf<'_>,
|
||||||
|
login_start: &LoginStart,
|
||||||
|
) -> Result<(), ()> {
|
||||||
|
packet::write_packet(
|
||||||
|
LoginSuccess {
|
||||||
|
uuid: uuid::offline_player_uuid(&login_start.name),
|
||||||
|
username: login_start.name.clone(),
|
||||||
|
},
|
||||||
|
client,
|
||||||
|
writer,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Play lobby ready sound effect if configured.
|
||||||
|
async fn play_lobby_ready_sound(
|
||||||
|
client: &Client,
|
||||||
|
client_info: &ClientInfo,
|
||||||
|
writer: &mut WriteHalf<'_>,
|
||||||
|
config: &Config,
|
||||||
|
) -> Result<(), ()> {
|
||||||
|
if let Some(sound_name) = config.join.lobby.ready_sound.as_ref() {
|
||||||
|
// Must not be empty string
|
||||||
|
if sound_name.trim().is_empty() {
|
||||||
|
warn!(target: "lazymc::lobby", "Lobby ready sound effect is an empty string, you should remove the configuration item instead");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Play sound effect
|
||||||
|
packets::play::player_pos::send(client, client_info, writer).await?;
|
||||||
|
packets::play::sound::send(client, client_info, writer, sound_name).await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Send packets to client to get workable play state for lobby world.
|
||||||
|
async fn send_lobby_play_packets(
|
||||||
|
client: &Client,
|
||||||
|
client_info: &ClientInfo,
|
||||||
|
writer: &mut WriteHalf<'_>,
|
||||||
|
server: &Server,
|
||||||
|
) -> Result<(), ()> {
|
||||||
|
// See: https://wiki.vg/Protocol_FAQ#What.27s_the_normal_login_sequence_for_a_client.3F
|
||||||
|
|
||||||
|
// Send initial game join
|
||||||
|
packets::play::join_game::lobby_send(client, client_info, writer, server).await?;
|
||||||
|
|
||||||
|
// Send server brand
|
||||||
|
packets::play::server_brand::send(client, client_info, writer).await?;
|
||||||
|
|
||||||
|
// Send spawn and player position, disables 'download terrain' screen
|
||||||
|
packets::play::player_pos::send(client, client_info, writer).await?;
|
||||||
|
|
||||||
|
// Notify client of world time, required once before keep-alive packets
|
||||||
|
packets::play::time_update::send(client, client_info, writer).await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An infinite keep-alive loop.
|
||||||
|
///
|
||||||
|
/// This will keep sending keep-alive and title packets to the client until it is dropped.
|
||||||
|
async fn keep_alive_loop(
|
||||||
|
client: &Client,
|
||||||
|
client_info: &ClientInfo,
|
||||||
|
writer: &mut WriteHalf<'_>,
|
||||||
|
config: &Config,
|
||||||
|
) -> Result<(), ()> {
|
||||||
|
let mut interval = time::interval(KEEP_ALIVE_INTERVAL);
|
||||||
|
|
||||||
|
loop {
|
||||||
|
interval.tick().await;
|
||||||
|
|
||||||
|
trace!(target: "lazymc::lobby", "Sending keep-alive sequence to lobby client");
|
||||||
|
|
||||||
|
// Send keep alive and title packets
|
||||||
|
packets::play::keep_alive::send(client, client_info, writer).await?;
|
||||||
|
packets::play::title::send(client, client_info, writer, &config.join.lobby.message).await?;
|
||||||
|
|
||||||
|
// TODO: verify we receive correct keep alive response
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Waiting stage.
|
||||||
|
///
|
||||||
|
/// In this stage we wait for the server to come online.
|
||||||
|
///
|
||||||
|
/// During this stage we keep sending keep-alive and title packets to the client to keep it active.
|
||||||
|
async fn stage_wait(
|
||||||
|
client: &Client,
|
||||||
|
client_info: &ClientInfo,
|
||||||
|
server: &Server,
|
||||||
|
config: &Config,
|
||||||
|
writer: &mut WriteHalf<'_>,
|
||||||
|
) -> Result<(), ()> {
|
||||||
|
select! {
|
||||||
|
a = keep_alive_loop(client, client_info, writer, config) => a,
|
||||||
|
b = wait_for_server(server, config) => b,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Wait for the server to come online.
|
||||||
|
///
|
||||||
|
/// Returns `Ok(())` once the server is online, returns `Err(())` if waiting failed.
|
||||||
|
async fn wait_for_server(server: &Server, config: &Config) -> Result<(), ()> {
|
||||||
|
debug!(target: "lazymc::lobby", "Waiting on server to come online...");
|
||||||
|
|
||||||
|
// 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::lobby", "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 | State::Stopped => {
|
||||||
|
break false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Wait for server state with timeout
|
||||||
|
let timeout = Duration::from_secs(config.join.lobby.timeout as u64);
|
||||||
|
match time::timeout(timeout, task_wait).await {
|
||||||
|
// Relay client to proxy
|
||||||
|
Ok(true) => {
|
||||||
|
debug!(target: "lazymc::lobby", "Server ready for lobby client");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Server stopping/stopped, this shouldn't happen, disconnect
|
||||||
|
Ok(false) => {}
|
||||||
|
|
||||||
|
// Timeout reached, disconnect
|
||||||
|
Err(_) => {
|
||||||
|
warn!(target: "lazymc::lobby", "Lobby client waiting for server to come online reached timeout of {}s", timeout.as_secs());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create connection to the server, with timeout.
|
||||||
|
///
|
||||||
|
/// This will initialize the connection to the play state. Client details are used.
|
||||||
|
async fn connect_to_server(
|
||||||
|
client_info: &ClientInfo,
|
||||||
|
inbound: &TcpStream,
|
||||||
|
config: &Config,
|
||||||
|
) -> Result<(Client, TcpStream, BytesMut), ()> {
|
||||||
|
time::timeout(
|
||||||
|
SERVER_CONNECT_TIMEOUT,
|
||||||
|
connect_to_server_no_timeout(client_info, inbound, config),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(|_| {
|
||||||
|
error!(target: "lazymc::lobby", "Creating new server connection for lobby client timed out after {}s", SERVER_CONNECT_TIMEOUT.as_secs());
|
||||||
|
})?
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create connection to the server, with no timeout.
|
||||||
|
///
|
||||||
|
/// This will initialize the connection to the play state. Client details are used.
|
||||||
|
// TODO: clean this up
|
||||||
|
async fn connect_to_server_no_timeout(
|
||||||
|
client_info: &ClientInfo,
|
||||||
|
inbound: &TcpStream,
|
||||||
|
config: &Config,
|
||||||
|
) -> Result<(Client, TcpStream, BytesMut), ()> {
|
||||||
|
// Open connection
|
||||||
|
// TODO: on connect fail, ping server and redirect to serve_status if offline
|
||||||
|
let mut outbound = TcpStream::connect(config.server.address)
|
||||||
|
.await
|
||||||
|
.map_err(|_| ())?;
|
||||||
|
|
||||||
|
// Add proxy header
|
||||||
|
if config.server.send_proxy_v2 {
|
||||||
|
trace!(target: "lazymc::lobby", "Sending client proxy header for server connection");
|
||||||
|
outbound
|
||||||
|
.write_all(&proxy::stream_proxy_header(inbound).map_err(|_| ())?)
|
||||||
|
.await
|
||||||
|
.map_err(|_| ())?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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();
|
||||||
|
|
||||||
|
// Replay client handshake packet
|
||||||
|
assert_eq!(
|
||||||
|
client_info.handshake.as_ref().unwrap().next_state,
|
||||||
|
ClientState::Login.to_id(),
|
||||||
|
"Client handshake should have login as next state"
|
||||||
|
);
|
||||||
|
packet::write_packet(
|
||||||
|
client_info.handshake.clone().unwrap(),
|
||||||
|
&tmp_client,
|
||||||
|
&mut writer,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// Request login start
|
||||||
|
packet::write_packet(
|
||||||
|
LoginStart {
|
||||||
|
name: client_info.username.clone().ok_or(())?,
|
||||||
|
},
|
||||||
|
&tmp_client,
|
||||||
|
&mut writer,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// Incoming buffer
|
||||||
|
let mut buf = BytesMut::new();
|
||||||
|
|
||||||
|
loop {
|
||||||
|
// Read packet from stream
|
||||||
|
let (packet, _raw) = match packet::read_packet(&tmp_client, &mut buf, &mut reader).await {
|
||||||
|
Ok(Some(packet)) => packet,
|
||||||
|
Ok(None) => break,
|
||||||
|
Err(_) => {
|
||||||
|
error!(target: "lazymc::lobby", "Closing connection, error occurred");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Grab client state
|
||||||
|
let client_state = tmp_client.state();
|
||||||
|
|
||||||
|
// Catch set compression
|
||||||
|
if client_state == ClientState::Login && packet.id == packets::login::CLIENT_SET_COMPRESSION
|
||||||
|
{
|
||||||
|
// Decode compression packet
|
||||||
|
let set_compression =
|
||||||
|
SetCompression::decode(&mut packet.data.as_slice()).map_err(|_| ())?;
|
||||||
|
|
||||||
|
// Client and server compression threshold should match, show warning if not
|
||||||
|
if set_compression.threshold != proto::COMPRESSION_THRESHOLD {
|
||||||
|
error!(
|
||||||
|
target: "lazymc::lobby",
|
||||||
|
"Compression threshold sent to lobby client does not match threshold from server, this may cause errors (client: {}, server: {})",
|
||||||
|
proto::COMPRESSION_THRESHOLD,
|
||||||
|
set_compression.threshold
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set client compression
|
||||||
|
tmp_client.set_compression(set_compression.threshold);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Catch encryption requests
|
||||||
|
if client_state == ClientState::Login
|
||||||
|
&& packet.id == packets::login::CLIENT_ENCRYPTION_REQUEST
|
||||||
|
{
|
||||||
|
error!(
|
||||||
|
target: "lazymc::lobby",
|
||||||
|
"Got encryption request from server, this is unsupported. Server must be in offline mode to use lobby.",
|
||||||
|
);
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hijack login plugin request
|
||||||
|
if client_state == ClientState::Login
|
||||||
|
&& packet.id == packets::login::CLIENT_LOGIN_PLUGIN_REQUEST
|
||||||
|
{
|
||||||
|
// Decode login plugin request
|
||||||
|
let plugin_request =
|
||||||
|
LoginPluginRequest::decode(&mut packet.data.as_slice()).map_err(|err| {
|
||||||
|
dbg!(err);
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// Respond with Forge messages
|
||||||
|
if config.server.forge {
|
||||||
|
trace!(target: "lazymc::lobby", "Got login plugin request from server, responding with Forge reply");
|
||||||
|
|
||||||
|
// Respond to Forge login plugin request
|
||||||
|
forge::respond_login_plugin_request(&tmp_client, plugin_request, &mut writer)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
warn!(target: "lazymc::lobby", "Got unexpected login plugin request from server, you may need to enable Forge support");
|
||||||
|
|
||||||
|
// Write unsuccesful login plugin response
|
||||||
|
packet::write_packet(
|
||||||
|
LoginPluginResponse {
|
||||||
|
message_id: plugin_request.message_id,
|
||||||
|
successful: false,
|
||||||
|
data: vec![],
|
||||||
|
},
|
||||||
|
&tmp_client,
|
||||||
|
&mut writer,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hijack login success
|
||||||
|
if client_state == ClientState::Login && packet.id == packets::login::CLIENT_LOGIN_SUCCESS {
|
||||||
|
trace!(target: "lazymc::lobby", "Got login success from server connection, change to play mode");
|
||||||
|
|
||||||
|
// TODO: parse this packet to ensure it's fine
|
||||||
|
// let login_success =
|
||||||
|
// LoginSuccess::decode(&mut packet.data.as_slice()).map_err(|err| {
|
||||||
|
// dbg!(err);
|
||||||
|
// ()
|
||||||
|
// })?;
|
||||||
|
|
||||||
|
// Switch to play state
|
||||||
|
tmp_client.set_state(ClientState::Play);
|
||||||
|
|
||||||
|
// Server must enable compression if enabled for client, show warning otherwise
|
||||||
|
if tmp_client.is_compressed() != (proto::COMPRESSION_THRESHOLD >= 0) {
|
||||||
|
error!(target: "lazymc::lobby", "Compression enabled for lobby client while the server did not, this will cause errors");
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok((tmp_client, outbound, buf));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hijack disconnect
|
||||||
|
if client_state == ClientState::Login && packet.id == packets::login::CLIENT_DISCONNECT {
|
||||||
|
error!(target: "lazymc::lobby", "Got disconnect from server connection");
|
||||||
|
|
||||||
|
// // Decode disconnect packet
|
||||||
|
// let login_disconnect =
|
||||||
|
// LoginDisconnect::decode(&mut packet.data.as_slice()).map_err(|err| {
|
||||||
|
// dbg!(err);
|
||||||
|
// })?;
|
||||||
|
|
||||||
|
// TODO: report/forward error to client
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show unhandled packet warning
|
||||||
|
debug!(target: "lazymc::lobby", "Got unhandled packet from server in connect_to_server:");
|
||||||
|
debug!(target: "lazymc::lobby", "- State: {:?}", client_state);
|
||||||
|
debug!(target: "lazymc::lobby", "- Packet ID: 0x{:02X} ({})", packet.id, packet.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gracefully close connection
|
||||||
|
net::close_tcp_stream(outbound).await.map_err(|_| ())?;
|
||||||
|
|
||||||
|
Err(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Wait for join game packet on server connection, with timeout.
|
||||||
|
///
|
||||||
|
/// This parses, consumes and returns the packet.
|
||||||
|
async fn wait_for_server_join_game(
|
||||||
|
client: &Client,
|
||||||
|
client_info: &ClientInfo,
|
||||||
|
outbound: &mut TcpStream,
|
||||||
|
buf: &mut BytesMut,
|
||||||
|
) -> Result<JoinGameData, ()> {
|
||||||
|
time::timeout(
|
||||||
|
SERVER_JOIN_GAME_TIMEOUT,
|
||||||
|
wait_for_server_join_game_no_timeout(client, client_info, outbound, buf),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(|_| {
|
||||||
|
error!(target: "lazymc::lobby", "Waiting for for game data from server for lobby client timed out after {}s", SERVER_JOIN_GAME_TIMEOUT.as_secs());
|
||||||
|
})?
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Wait for join game packet on server connection, with no timeout.
|
||||||
|
///
|
||||||
|
/// This parses, consumes and returns the packet.
|
||||||
|
// TODO: clean this up
|
||||||
|
// TODO: do not drop error here, return Box<dyn Error>
|
||||||
|
async fn wait_for_server_join_game_no_timeout(
|
||||||
|
client: &Client,
|
||||||
|
client_info: &ClientInfo,
|
||||||
|
outbound: &mut TcpStream,
|
||||||
|
buf: &mut BytesMut,
|
||||||
|
) -> Result<JoinGameData, ()> {
|
||||||
|
let (mut reader, mut _writer) = outbound.split();
|
||||||
|
|
||||||
|
loop {
|
||||||
|
// Read packet from stream
|
||||||
|
let (packet, _raw) = match packet::read_packet(client, buf, &mut reader).await {
|
||||||
|
Ok(Some(packet)) => packet,
|
||||||
|
Ok(None) => break,
|
||||||
|
Err(_) => {
|
||||||
|
error!(target: "lazymc::lobby", "Closing connection, error occurred");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Catch join game
|
||||||
|
if packets::play::join_game::is_packet(client_info, packet.id) {
|
||||||
|
// Parse join game data
|
||||||
|
let join_game_data = JoinGameData::from_packet(client_info, packet).map_err(|err| {
|
||||||
|
warn!(target: "lazymc::lobby", "Failed to parse join game packet: {:?}", err);
|
||||||
|
})?;
|
||||||
|
|
||||||
|
return Ok(join_game_data);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show unhandled packet warning
|
||||||
|
debug!(target: "lazymc::lobby", "Got unhandled packet from server in wait_for_server_join_game:");
|
||||||
|
debug!(target: "lazymc::lobby", "- Packet ID: 0x{:02X} ({})", packet.id, packet.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gracefully close connection
|
||||||
|
net::close_tcp_stream_ref(outbound).await.map_err(|_| ())?;
|
||||||
|
|
||||||
|
Err(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Route our lobby client through the proxy to the real server, spawning a new task.
|
||||||
|
///
|
||||||
|
/// `inbound_queue` is used for data already received from the server, that needs to be pushed to
|
||||||
|
/// the client.
|
||||||
|
#[inline]
|
||||||
|
pub fn route_proxy(inbound: TcpStream, outbound: TcpStream, inbound_queue: BytesMut) {
|
||||||
|
// When server is online, proxy all
|
||||||
|
let service = async move {
|
||||||
|
proxy::proxy_inbound_outbound_with_queue(inbound, outbound, &inbound_queue, &[])
|
||||||
|
.map(|r| {
|
||||||
|
if let Err(err) = r {
|
||||||
|
warn!(target: "lazymc", "Failed to proxy: {}", err);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
};
|
||||||
|
|
||||||
|
tokio::spawn(service);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Drain given reader until nothing is left voiding all data.
|
||||||
|
async fn drain_stream(reader: &mut ReadHalf<'_>) -> Result<(), ()> {
|
||||||
|
let mut drain_buf = [0; 8 * 1024];
|
||||||
|
loop {
|
||||||
|
match reader.try_read(&mut drain_buf) {
|
||||||
|
Ok(read) if read == 0 => return Ok(()),
|
||||||
|
Err(err) if err.kind() == ErrorKind::WouldBlock => return Ok(()),
|
||||||
|
Ok(_) => continue,
|
||||||
|
Err(err) => {
|
||||||
|
error!(target: "lazymc::lobby", "Failed to drain lobby client connection before relaying to real server. Maybe already disconnected? Error: {:?}", err);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
14
src/main.rs
14
src/main.rs
@@ -10,9 +10,15 @@ extern crate log;
|
|||||||
pub(crate) mod action;
|
pub(crate) mod action;
|
||||||
pub(crate) mod cli;
|
pub(crate) mod cli;
|
||||||
pub(crate) mod config;
|
pub(crate) mod config;
|
||||||
|
pub(crate) mod forge;
|
||||||
|
pub(crate) mod join;
|
||||||
|
#[cfg(feature = "lobby")]
|
||||||
|
pub(crate) mod lobby;
|
||||||
pub(crate) mod mc;
|
pub(crate) mod mc;
|
||||||
pub(crate) mod monitor;
|
pub(crate) mod monitor;
|
||||||
|
pub(crate) mod net;
|
||||||
pub(crate) mod os;
|
pub(crate) mod os;
|
||||||
|
pub(crate) mod probe;
|
||||||
pub(crate) mod proto;
|
pub(crate) mod proto;
|
||||||
pub(crate) mod proxy;
|
pub(crate) mod proxy;
|
||||||
pub(crate) mod server;
|
pub(crate) mod server;
|
||||||
@@ -23,7 +29,11 @@ pub(crate) mod util;
|
|||||||
|
|
||||||
use std::env;
|
use std::env;
|
||||||
|
|
||||||
use clap::App;
|
use clap::Command;
|
||||||
|
|
||||||
|
// Compile time feature compatability check.
|
||||||
|
#[cfg(all(windows, not(feature = "rcon")))]
|
||||||
|
compile_error!("Must enable \"rcon\" feature on Windows.");
|
||||||
|
|
||||||
/// Default log level if none is set.
|
/// Default log level if none is set.
|
||||||
const LOG_DEFAULT: &str = "info";
|
const LOG_DEFAULT: &str = "info";
|
||||||
@@ -53,7 +63,7 @@ fn init_log() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Invoke an action.
|
/// Invoke an action.
|
||||||
fn invoke_action(app: App) -> Result<(), ()> {
|
fn invoke_action(app: Command) -> Result<(), ()> {
|
||||||
let matches = app.get_matches();
|
let matches = app.get_matches();
|
||||||
|
|
||||||
// Config operations
|
// Config operations
|
||||||
|
99
src/mc/ban.rs
Normal file
99
src/mc/ban.rs
Normal 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 })
|
||||||
|
}
|
112
src/mc/dimension.rs
Normal file
112
src/mc/dimension.rs
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
use nbt::CompoundTag;
|
||||||
|
|
||||||
|
/// Create lobby dimension from the given codec.
|
||||||
|
///
|
||||||
|
/// This creates a dimension suitable for the lobby that should be suitable for the current server
|
||||||
|
/// version.
|
||||||
|
pub fn lobby_dimension(codec: &CompoundTag) -> CompoundTag {
|
||||||
|
// Retrieve dimension types from codec
|
||||||
|
let dimension_types = match codec.get_compound_tag("minecraft:dimension_type") {
|
||||||
|
Ok(types) => types,
|
||||||
|
Err(_) => return lobby_default_dimension(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get base dimension
|
||||||
|
let mut base = lobby_base_dimension(dimension_types);
|
||||||
|
|
||||||
|
// Change known properties on base to get more desirable dimension
|
||||||
|
base.insert_i8("piglin_safe", 1);
|
||||||
|
base.insert_f32("ambient_light", 0.0);
|
||||||
|
// base.insert_str("infiniburn", "minecraft:infiniburn_end");
|
||||||
|
base.insert_i8("respawn_anchor_works", 0);
|
||||||
|
base.insert_i8("has_skylight", 0);
|
||||||
|
base.insert_i8("bed_works", 0);
|
||||||
|
base.insert_str("effects", "minecraft:the_end");
|
||||||
|
base.insert_i64("fixed_time", 0);
|
||||||
|
base.insert_i8("has_raids", 0);
|
||||||
|
base.insert_i32("min_y", 0);
|
||||||
|
base.insert_i32("height", 16);
|
||||||
|
base.insert_i32("logical_height", 16);
|
||||||
|
base.insert_f64("coordinate_scale", 1.0);
|
||||||
|
base.insert_i8("ultrawarm", 0);
|
||||||
|
base.insert_i8("has_ceiling", 0);
|
||||||
|
|
||||||
|
base
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get lobby base dimension.
|
||||||
|
///
|
||||||
|
/// This retrieves the most desirable dimension to use as base for the lobby from the given list of
|
||||||
|
/// `dimension_types`.
|
||||||
|
///
|
||||||
|
/// If no dimension is found in the given tag, a default one will be returned.
|
||||||
|
fn lobby_base_dimension(dimension_types: &CompoundTag) -> CompoundTag {
|
||||||
|
// The dimension types we prefer the most, in order
|
||||||
|
let preferred = vec![
|
||||||
|
"minecraft:the_end",
|
||||||
|
"minecraft:the_nether",
|
||||||
|
"minecraft:the_overworld",
|
||||||
|
];
|
||||||
|
|
||||||
|
let dimensions = dimension_types.get_compound_tag_vec("value").unwrap();
|
||||||
|
|
||||||
|
for name in preferred {
|
||||||
|
if let Some(dimension) = dimensions
|
||||||
|
.iter()
|
||||||
|
.find(|d| d.get_str("name").map(|n| n == name).unwrap_or(false))
|
||||||
|
{
|
||||||
|
if let Ok(dimension) = dimension.get_compound_tag("element") {
|
||||||
|
return dimension.clone();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return first dimension
|
||||||
|
if let Some(dimension) = dimensions.first() {
|
||||||
|
if let Ok(dimension) = dimension.get_compound_tag("element") {
|
||||||
|
return dimension.clone();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fall back to default dimension
|
||||||
|
lobby_default_dimension()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Default lobby dimension codec from resource file.
|
||||||
|
///
|
||||||
|
/// This likely breaks if the Minecraft version doesn't match exactly.
|
||||||
|
/// Please use an up-to-date coded from the server instead.
|
||||||
|
pub fn default_dimension_codec() -> CompoundTag {
|
||||||
|
snbt_to_compound_tag(include_str!("../../res/dimension_codec.snbt"))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Default lobby dimension from resource file.
|
||||||
|
///
|
||||||
|
/// This likely breaks if the Minecraft version doesn't match exactly.
|
||||||
|
/// Please use `lobby_dimension` with an up-to-date coded from the server instead.
|
||||||
|
fn lobby_default_dimension() -> CompoundTag {
|
||||||
|
snbt_to_compound_tag(include_str!("../../res/dimension.snbt"))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read NBT CompoundTag from SNBT.
|
||||||
|
fn snbt_to_compound_tag(data: &str) -> CompoundTag {
|
||||||
|
use quartz_nbt::io::{write_nbt, Flavor};
|
||||||
|
use quartz_nbt::snbt;
|
||||||
|
|
||||||
|
// Parse SNBT data
|
||||||
|
let compound = snbt::parse(data).expect("failed to parse SNBT");
|
||||||
|
|
||||||
|
// Encode to binary
|
||||||
|
let mut binary = Vec::new();
|
||||||
|
write_nbt(&mut binary, None, &compound, Flavor::Uncompressed)
|
||||||
|
.expect("failed to encode NBT CompoundTag as binary");
|
||||||
|
|
||||||
|
// Parse binary with usable NBT create
|
||||||
|
bin_to_compound_tag(&binary)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read NBT CompoundTag from SNBT.
|
||||||
|
fn bin_to_compound_tag(data: &[u8]) -> CompoundTag {
|
||||||
|
use nbt::decode::read_compound_tag;
|
||||||
|
read_compound_tag(&mut &*data).unwrap()
|
||||||
|
}
|
32
src/mc/favicon.rs
Normal file
32
src/mc/favicon.rs
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
use base64::Engine;
|
||||||
|
|
||||||
|
use crate::proto::client::ClientInfo;
|
||||||
|
|
||||||
|
/// Protocol version since when favicons are supported.
|
||||||
|
const FAVICON_PROTOCOL_VERSION: u32 = 4;
|
||||||
|
|
||||||
|
/// Get default server status favicon.
|
||||||
|
pub fn default_favicon() -> String {
|
||||||
|
encode_favicon(include_bytes!("../../res/unknown_server_optimized.png"))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Encode favicon bytes to a string Minecraft can read.
|
||||||
|
///
|
||||||
|
/// This assumes the favicon data to be a valid PNG image.
|
||||||
|
pub fn encode_favicon(data: &[u8]) -> String {
|
||||||
|
format!(
|
||||||
|
"{}{}",
|
||||||
|
"data:image/png;base64,",
|
||||||
|
base64::engine::general_purpose::STANDARD.encode(data)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check whether the status response favicon is supported based on the given client info.
|
||||||
|
///
|
||||||
|
/// Defaults to `true` if unsure.
|
||||||
|
pub fn supports_favicon(client_info: &ClientInfo) -> bool {
|
||||||
|
client_info
|
||||||
|
.protocol
|
||||||
|
.map(|p| p >= FAVICON_PROTOCOL_VERSION)
|
||||||
|
.unwrap_or(true)
|
||||||
|
}
|
@@ -1,3 +1,14 @@
|
|||||||
|
pub mod ban;
|
||||||
|
#[cfg(feature = "lobby")]
|
||||||
|
pub mod dimension;
|
||||||
|
pub mod favicon;
|
||||||
#[cfg(feature = "rcon")]
|
#[cfg(feature = "rcon")]
|
||||||
pub mod rcon;
|
pub mod rcon;
|
||||||
pub mod server_properties;
|
pub mod server_properties;
|
||||||
|
#[cfg(feature = "lobby")]
|
||||||
|
pub mod uuid;
|
||||||
|
pub mod whitelist;
|
||||||
|
|
||||||
|
/// Minecraft ticks per second.
|
||||||
|
#[allow(unused)]
|
||||||
|
pub const TICKS_PER_SECOND: u32 = 20;
|
||||||
|
@@ -1,8 +1,13 @@
|
|||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use async_std::net::TcpStream;
|
||||||
|
use async_std::prelude::*;
|
||||||
use rust_rcon::{Connection, Error as RconError};
|
use rust_rcon::{Connection, Error as RconError};
|
||||||
use tokio::time;
|
use tokio::time;
|
||||||
|
|
||||||
|
use crate::config::Config;
|
||||||
|
use crate::proxy;
|
||||||
|
|
||||||
/// Minecraft RCON quirk.
|
/// Minecraft RCON quirk.
|
||||||
///
|
///
|
||||||
/// Wait this time between RCON operations.
|
/// Wait this time between RCON operations.
|
||||||
@@ -17,16 +22,39 @@ pub struct Rcon {
|
|||||||
|
|
||||||
impl Rcon {
|
impl Rcon {
|
||||||
/// Connect to a host.
|
/// Connect to a host.
|
||||||
pub async fn connect(addr: &str, pass: &str) -> Result<Self, Box<dyn std::error::Error>> {
|
pub async fn connect(
|
||||||
|
config: &Config,
|
||||||
|
addr: &str,
|
||||||
|
pass: &str,
|
||||||
|
) -> Result<Self, Box<dyn std::error::Error>> {
|
||||||
|
// Connect to our TCP stream
|
||||||
|
let mut stream = TcpStream::connect(addr).await?;
|
||||||
|
|
||||||
|
// Add proxy header
|
||||||
|
if config.rcon.send_proxy_v2 {
|
||||||
|
trace!(target: "lazymc::rcon", "Sending local proxy header for RCON connection");
|
||||||
|
stream.write_all(&proxy::local_proxy_header()?).await?;
|
||||||
|
}
|
||||||
|
|
||||||
// Start connection
|
// Start connection
|
||||||
let con = Connection::builder()
|
let con = Connection::builder()
|
||||||
.enable_minecraft_quirks(true)
|
.enable_minecraft_quirks(true)
|
||||||
.connect(addr, pass)
|
.connect_stream(stream, pass)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
Ok(Self { con })
|
Ok(Self { con })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Connect to a host from the given configuration.
|
||||||
|
pub async fn connect_config(config: &Config) -> Result<Self, Box<dyn std::error::Error>> {
|
||||||
|
// RCON address
|
||||||
|
let mut addr = config.server.address;
|
||||||
|
addr.set_port(config.rcon.port);
|
||||||
|
let addr = addr.to_string();
|
||||||
|
|
||||||
|
Self::connect(config, &addr, &config.rcon.password).await
|
||||||
|
}
|
||||||
|
|
||||||
/// Send command over RCON.
|
/// Send command over RCON.
|
||||||
pub async fn cmd(&mut self, cmd: &str) -> Result<String, RconError> {
|
pub async fn cmd(&mut self, cmd: &str) -> Result<String, RconError> {
|
||||||
// Minecraft quirk
|
// Minecraft quirk
|
||||||
|
@@ -11,39 +11,39 @@ const EOL: &str = "\r\n";
|
|||||||
/// Try to rewrite changes in server.properties file in dir.
|
/// Try to rewrite changes in server.properties file in dir.
|
||||||
///
|
///
|
||||||
/// Prints an error and stops on failure.
|
/// Prints an error and stops on failure.
|
||||||
pub fn rewrite_dir(dir: &Path, changes: HashMap<&str, String>) {
|
pub fn rewrite_dir<P: AsRef<Path>>(dir: P, changes: HashMap<&str, String>) {
|
||||||
if changes.is_empty() {
|
if changes.is_empty() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure directory exists
|
// Ensure directory exists
|
||||||
if !dir.is_dir() {
|
if !dir.as_ref().is_dir() {
|
||||||
warn!(target: "lazymc",
|
warn!(target: "lazymc",
|
||||||
"Not rewriting {} file, configured server directory doesn't exist: {}",
|
"Not rewriting {} file, configured server directory doesn't exist: {}",
|
||||||
FILE,
|
FILE,
|
||||||
dir.to_str().unwrap_or("?")
|
dir.as_ref().to_str().unwrap_or("?")
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Rewrite file
|
// Rewrite file
|
||||||
rewrite_file(&dir.join(FILE), changes)
|
rewrite_file(dir.as_ref().join(FILE), changes)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Try to rewrite changes in server.properties file.
|
/// Try to rewrite changes in server.properties file.
|
||||||
///
|
///
|
||||||
/// Prints an error and stops on failure.
|
/// Prints an error and stops on failure.
|
||||||
pub fn rewrite_file(file: &Path, changes: HashMap<&str, String>) {
|
pub fn rewrite_file<P: AsRef<Path>>(file: P, changes: HashMap<&str, String>) {
|
||||||
if changes.is_empty() {
|
if changes.is_empty() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// File must exist
|
// File must exist
|
||||||
if !file.is_file() {
|
if !file.as_ref().is_file() {
|
||||||
warn!(target: "lazymc",
|
warn!(target: "lazymc",
|
||||||
"Not writing {} file, not found at: {}",
|
"Not writing {} file, not found at: {}",
|
||||||
FILE,
|
FILE,
|
||||||
file.to_str().unwrap_or("?"),
|
file.as_ref().to_str().unwrap_or("?"),
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -114,7 +114,7 @@ fn rewrite_contents(contents: String, mut changes: HashMap<&str, String>) -> Opt
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Try to split property
|
// Try to split property
|
||||||
let (key, value) = match line.split_once("=") {
|
let (key, value) = match line.split_once('=') {
|
||||||
Some(result) => result,
|
Some(result) => result,
|
||||||
None => return line,
|
None => return line,
|
||||||
};
|
};
|
||||||
@@ -122,7 +122,7 @@ fn rewrite_contents(contents: String, mut changes: HashMap<&str, String>) -> Opt
|
|||||||
// Take any new value, and update it
|
// Take any new value, and update it
|
||||||
if let Some((_, new)) = changes.remove_entry(key.trim().to_lowercase().as_str()) {
|
if let Some((_, new)) = changes.remove_entry(key.trim().to_lowercase().as_str()) {
|
||||||
if value != new {
|
if value != new {
|
||||||
line = format!("{}={}", key, new);
|
line = format!("{key}={new}");
|
||||||
changed = true;
|
changed = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -134,7 +134,7 @@ fn rewrite_contents(contents: String, mut changes: HashMap<&str, String>) -> Opt
|
|||||||
|
|
||||||
// Append any missed changes
|
// Append any missed changes
|
||||||
for (key, value) in changes {
|
for (key, value) in changes {
|
||||||
new_contents += &format!("{}{}={}", EOL, key, value);
|
new_contents += &format!("{EOL}{key}={value}");
|
||||||
changed = true;
|
changed = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -145,3 +145,37 @@ fn rewrite_contents(contents: String, mut changes: HashMap<&str, String>) -> Opt
|
|||||||
None
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Read the given property from the given server.properties file.o
|
||||||
|
///
|
||||||
|
/// Returns `None` if file does not contain the property.
|
||||||
|
pub fn read_property<P: AsRef<Path>>(file: P, property: &str) -> Option<String> {
|
||||||
|
// File must exist
|
||||||
|
if !file.as_ref().is_file() {
|
||||||
|
warn!(target: "lazymc",
|
||||||
|
"Failed to read property from {} file, it does not exist",
|
||||||
|
FILE,
|
||||||
|
);
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read contents
|
||||||
|
let contents = match fs::read_to_string(&file) {
|
||||||
|
Ok(contents) => contents,
|
||||||
|
Err(err) => {
|
||||||
|
error!(target: "lazymc",
|
||||||
|
"Failed to read property from {} file, could not load: {}",
|
||||||
|
FILE,
|
||||||
|
err,
|
||||||
|
);
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Find property, return value
|
||||||
|
contents
|
||||||
|
.lines()
|
||||||
|
.filter_map(|line| line.split_once('='))
|
||||||
|
.find(|(p, _)| p.trim().to_lowercase() == property.to_lowercase())
|
||||||
|
.map(|(_, v)| v.trim().to_string())
|
||||||
|
}
|
||||||
|
33
src/mc/uuid.rs
Normal file
33
src/mc/uuid.rs
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
use md5::{Digest, Md5};
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
/// Offline player namespace.
|
||||||
|
const OFFLINE_PLAYER_NAMESPACE: &str = "OfflinePlayer:";
|
||||||
|
|
||||||
|
/// Get UUID for given player username.
|
||||||
|
fn player_uuid(username: &str) -> Uuid {
|
||||||
|
java_name_uuid_from_bytes(username.as_bytes())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get UUID for given offline player username.
|
||||||
|
pub fn offline_player_uuid(username: &str) -> Uuid {
|
||||||
|
player_uuid(&format!("{OFFLINE_PLAYER_NAMESPACE}{username}"))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Java's `UUID.nameUUIDFromBytes`
|
||||||
|
///
|
||||||
|
/// Static factory to retrieve a type 3 (name based) `Uuid` based on the specified byte array.
|
||||||
|
///
|
||||||
|
/// Ported from: <https://git.io/J1b6A>
|
||||||
|
fn java_name_uuid_from_bytes(data: &[u8]) -> Uuid {
|
||||||
|
let mut hasher = Md5::new();
|
||||||
|
hasher.update(data);
|
||||||
|
let mut md5: [u8; 16] = hasher.finalize().into();
|
||||||
|
|
||||||
|
md5[6] &= 0x0f; // clear version
|
||||||
|
md5[6] |= 0x30; // set to version 3
|
||||||
|
md5[8] &= 0x3f; // clear variant
|
||||||
|
md5[8] |= 0x80; // set to IETF variant
|
||||||
|
|
||||||
|
Uuid::from_bytes(md5)
|
||||||
|
}
|
107
src/mc/whitelist.rs
Normal file
107
src/mc/whitelist.rs
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
use std::error::Error;
|
||||||
|
use std::fs;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
/// Whitelist file name.
|
||||||
|
pub const WHITELIST_FILE: &str = "whitelist.json";
|
||||||
|
|
||||||
|
/// OPs file name.
|
||||||
|
pub const OPS_FILE: &str = "ops.json";
|
||||||
|
|
||||||
|
/// Whitelisted users.
|
||||||
|
///
|
||||||
|
/// Includes list of OPs, which are also automatically whitelisted.
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
pub struct Whitelist {
|
||||||
|
/// Whitelisted users.
|
||||||
|
whitelist: Vec<String>,
|
||||||
|
|
||||||
|
/// OPd users.
|
||||||
|
ops: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Whitelist {
|
||||||
|
/// Check whether a user is whitelisted.
|
||||||
|
pub fn is_whitelisted(&self, username: &str) -> bool {
|
||||||
|
self.whitelist.iter().any(|u| u == username) || self.ops.iter().any(|u| u == username)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A whitelist user.
|
||||||
|
#[derive(Debug, Deserialize, Clone)]
|
||||||
|
pub struct WhitelistUser {
|
||||||
|
/// Whitelisted username.
|
||||||
|
#[serde(rename = "name", alias = "username")]
|
||||||
|
pub username: String,
|
||||||
|
|
||||||
|
/// Whitelisted UUID.
|
||||||
|
pub uuid: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An OP user.
|
||||||
|
#[derive(Debug, Deserialize, Clone)]
|
||||||
|
pub struct OpUser {
|
||||||
|
/// OP username.
|
||||||
|
#[serde(rename = "name", alias = "username")]
|
||||||
|
pub username: String,
|
||||||
|
|
||||||
|
/// OP UUID.
|
||||||
|
pub uuid: Option<String>,
|
||||||
|
|
||||||
|
/// OP level.
|
||||||
|
pub level: Option<u32>,
|
||||||
|
|
||||||
|
/// Whether OP can bypass player limit.
|
||||||
|
#[serde(rename = "bypassesPlayerLimit")]
|
||||||
|
pub byapsses_player_limit: Option<bool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Load whitelist from directory.
|
||||||
|
pub fn load_dir(path: &Path) -> Result<Whitelist, Box<dyn Error>> {
|
||||||
|
let whitelist_file = path.join(WHITELIST_FILE);
|
||||||
|
let ops_file = path.join(OPS_FILE);
|
||||||
|
|
||||||
|
// Load whitelist users
|
||||||
|
let whitelist = if whitelist_file.is_file() {
|
||||||
|
load_whitelist(&whitelist_file)?
|
||||||
|
} else {
|
||||||
|
vec![]
|
||||||
|
};
|
||||||
|
|
||||||
|
// Load OPd users
|
||||||
|
let ops = if ops_file.is_file() {
|
||||||
|
load_ops(&ops_file)?
|
||||||
|
} else {
|
||||||
|
vec![]
|
||||||
|
};
|
||||||
|
|
||||||
|
debug!(target: "lazymc", "Loaded {} whitelist and {} OP users", whitelist.len(), ops.len());
|
||||||
|
|
||||||
|
Ok(Whitelist { whitelist, ops })
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Load whitelist from file.
|
||||||
|
fn load_whitelist(path: &Path) -> Result<Vec<String>, Box<dyn Error>> {
|
||||||
|
// Load file contents
|
||||||
|
let contents = fs::read_to_string(path)?;
|
||||||
|
|
||||||
|
// Parse contents
|
||||||
|
let users: Vec<WhitelistUser> = serde_json::from_str(&contents)?;
|
||||||
|
|
||||||
|
// Pluck usernames
|
||||||
|
Ok(users.into_iter().map(|user| user.username).collect())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Load OPs from file.
|
||||||
|
fn load_ops(path: &Path) -> Result<Vec<String>, Box<dyn Error>> {
|
||||||
|
// Load file contents
|
||||||
|
let contents = fs::read_to_string(path)?;
|
||||||
|
|
||||||
|
// Parse contents
|
||||||
|
let users: Vec<OpUser> = serde_json::from_str(&contents)?;
|
||||||
|
|
||||||
|
// Pluck usernames
|
||||||
|
Ok(users.into_iter().map(|user| user.username).collect())
|
||||||
|
}
|
129
src/monitor.rs
129
src/monitor.rs
@@ -1,5 +1,3 @@
|
|||||||
// TODO: remove all unwraps/expects here!
|
|
||||||
|
|
||||||
use std::net::SocketAddr;
|
use std::net::SocketAddr;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
@@ -7,16 +5,19 @@ use std::time::Duration;
|
|||||||
use bytes::BytesMut;
|
use bytes::BytesMut;
|
||||||
use minecraft_protocol::data::server_status::ServerStatus;
|
use minecraft_protocol::data::server_status::ServerStatus;
|
||||||
use minecraft_protocol::decoder::Decoder;
|
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::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 rand::Rng;
|
||||||
use tokio::io::AsyncWriteExt;
|
use tokio::io::AsyncWriteExt;
|
||||||
use tokio::net::TcpStream;
|
use tokio::net::TcpStream;
|
||||||
use tokio::time;
|
use tokio::time;
|
||||||
|
|
||||||
use crate::config::Config;
|
use crate::config::Config;
|
||||||
use crate::proto::{self, ClientState, RawPacket};
|
use crate::proto::client::{Client, ClientState};
|
||||||
|
use crate::proto::{packet, packets};
|
||||||
|
use crate::proxy;
|
||||||
use crate::server::{Server, State};
|
use crate::server::{Server, State};
|
||||||
|
|
||||||
/// Monitor ping inverval in seconds.
|
/// Monitor ping inverval in seconds.
|
||||||
@@ -26,7 +27,7 @@ const MONITOR_POLL_INTERVAL: Duration = Duration::from_secs(2);
|
|||||||
const STATUS_TIMEOUT: u64 = 20;
|
const STATUS_TIMEOUT: u64 = 20;
|
||||||
|
|
||||||
/// Ping request timeout in seconds.
|
/// Ping request timeout in seconds.
|
||||||
const PING_TIMEOUT: u64 = 20;
|
const PING_TIMEOUT: u64 = 10;
|
||||||
|
|
||||||
/// Monitor server.
|
/// Monitor server.
|
||||||
pub async fn monitor_server(config: Arc<Config>, server: Arc<Server>) {
|
pub async fn monitor_server(config: Arc<Config>, server: Arc<Server>) {
|
||||||
@@ -56,13 +57,13 @@ pub async fn monitor_server(config: Arc<Config>, server: Arc<Server>) {
|
|||||||
|
|
||||||
// Sleep server when it's bedtime
|
// Sleep server when it's bedtime
|
||||||
if server.should_sleep(&config).await {
|
if server.should_sleep(&config).await {
|
||||||
info!(target: "lazymc::montior", "Server has been idle, sleeping...");
|
info!(target: "lazymc::monitor", "Server has been idle, sleeping...");
|
||||||
server.stop(&config).await;
|
server.stop(&config).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check whether we should force kill server
|
// Check whether we should force kill server
|
||||||
if server.should_kill().await {
|
if server.should_kill().await {
|
||||||
error!(target: "lazymc::montior", "Force killing server, took too long to start or stop");
|
error!(target: "lazymc::monitor", "Force killing server, took too long to start or stop");
|
||||||
if !server.force_kill().await {
|
if !server.force_kill().await {
|
||||||
warn!(target: "lazymc", "Failed to force kill server");
|
warn!(target: "lazymc", "Failed to force kill server");
|
||||||
}
|
}
|
||||||
@@ -86,6 +87,7 @@ pub async fn poll_server(
|
|||||||
|
|
||||||
// Try ping fallback if server is currently started
|
// Try ping fallback if server is currently started
|
||||||
if server.state() == State::Started {
|
if server.state() == State::Started {
|
||||||
|
debug!(target: "lazymc::monitor", "Failed to get status from started server, trying ping...");
|
||||||
do_ping(config, addr).await?;
|
do_ping(config, addr).await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,84 +98,92 @@ pub async fn poll_server(
|
|||||||
async fn fetch_status(config: &Config, addr: SocketAddr) -> Result<ServerStatus, ()> {
|
async fn fetch_status(config: &Config, addr: SocketAddr) -> Result<ServerStatus, ()> {
|
||||||
let mut stream = TcpStream::connect(addr).await.map_err(|_| ())?;
|
let mut stream = TcpStream::connect(addr).await.map_err(|_| ())?;
|
||||||
|
|
||||||
send_handshake(&mut stream, config, addr).await?;
|
// Add proxy header
|
||||||
request_status(&mut stream).await?;
|
if config.server.send_proxy_v2 {
|
||||||
wait_for_status_timeout(&mut stream).await
|
trace!(target: "lazymc::monitor", "Sending local proxy header for server connection");
|
||||||
|
stream
|
||||||
|
.write_all(&proxy::local_proxy_header().map_err(|_| ())?)
|
||||||
|
.await
|
||||||
|
.map_err(|_| ())?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dummy client
|
||||||
|
let client = Client::dummy();
|
||||||
|
|
||||||
|
send_handshake(&client, &mut stream, config, addr).await?;
|
||||||
|
request_status(&client, &mut stream).await?;
|
||||||
|
wait_for_status_timeout(&client, &mut stream).await
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Attemp to ping server.
|
/// Attemp to ping server.
|
||||||
async fn do_ping(config: &Config, addr: SocketAddr) -> Result<(), ()> {
|
async fn do_ping(config: &Config, addr: SocketAddr) -> Result<(), ()> {
|
||||||
let mut stream = TcpStream::connect(addr).await.map_err(|_| ())?;
|
let mut stream = TcpStream::connect(addr).await.map_err(|_| ())?;
|
||||||
|
|
||||||
send_handshake(&mut stream, config, addr).await?;
|
// Add proxy header
|
||||||
let token = send_ping(&mut stream).await?;
|
if config.server.send_proxy_v2 {
|
||||||
wait_for_ping_timeout(&mut stream, token).await
|
trace!(target: "lazymc::monitor", "Sending local proxy header for server connection");
|
||||||
|
stream
|
||||||
|
.write_all(&proxy::local_proxy_header().map_err(|_| ())?)
|
||||||
|
.await
|
||||||
|
.map_err(|_| ())?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dummy client
|
||||||
|
let client = Client::dummy();
|
||||||
|
|
||||||
|
send_handshake(&client, &mut stream, config, addr).await?;
|
||||||
|
let token = send_ping(&client, &mut stream).await?;
|
||||||
|
wait_for_ping_timeout(&client, &mut stream, token).await
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Send handshake.
|
/// Send handshake.
|
||||||
async fn send_handshake(
|
async fn send_handshake(
|
||||||
|
client: &Client,
|
||||||
stream: &mut TcpStream,
|
stream: &mut TcpStream,
|
||||||
config: &Config,
|
config: &Config,
|
||||||
addr: SocketAddr,
|
addr: SocketAddr,
|
||||||
) -> Result<(), ()> {
|
) -> Result<(), ()> {
|
||||||
let handshake = Handshake {
|
packet::write_packet(
|
||||||
protocol_version: config.public.protocol as i32,
|
Handshake {
|
||||||
server_addr: addr.ip().to_string(),
|
protocol_version: config.public.protocol as i32,
|
||||||
server_port: addr.port(),
|
server_addr: addr.ip().to_string(),
|
||||||
next_state: ClientState::Status.to_id(),
|
server_port: addr.port(),
|
||||||
};
|
next_state: ClientState::Status.to_id(),
|
||||||
|
},
|
||||||
let mut packet = Vec::new();
|
client,
|
||||||
handshake.encode(&mut packet).map_err(|_| ())?;
|
&mut stream.split().1,
|
||||||
|
)
|
||||||
let raw = RawPacket::new(proto::HANDSHAKE_PACKET_ID_HANDSHAKE, packet)
|
.await
|
||||||
.encode()
|
|
||||||
.map_err(|_| ())?;
|
|
||||||
stream.write_all(&raw).await.map_err(|_| ())?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Send status request.
|
/// Send status request.
|
||||||
async fn request_status(stream: &mut TcpStream) -> Result<(), ()> {
|
async fn request_status(client: &Client, stream: &mut TcpStream) -> Result<(), ()> {
|
||||||
let raw = RawPacket::new(proto::STATUS_PACKET_ID_STATUS, vec![])
|
packet::write_packet(StatusRequest {}, client, &mut stream.split().1).await
|
||||||
.encode()
|
|
||||||
.map_err(|_| ())?;
|
|
||||||
stream.write_all(&raw).await.map_err(|_| ())?;
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Send status request.
|
/// Send status request.
|
||||||
async fn send_ping(stream: &mut TcpStream) -> Result<u64, ()> {
|
async fn send_ping(client: &Client, stream: &mut TcpStream) -> Result<u64, ()> {
|
||||||
let token = rand::thread_rng().gen();
|
let token = rand::thread_rng().gen();
|
||||||
let ping = PingRequest { time: token };
|
packet::write_packet(PingRequest { time: token }, client, &mut stream.split().1).await?;
|
||||||
|
|
||||||
let mut packet = Vec::new();
|
|
||||||
ping.encode(&mut packet).map_err(|_| ())?;
|
|
||||||
|
|
||||||
let raw = RawPacket::new(proto::STATUS_PACKET_ID_PING, packet)
|
|
||||||
.encode()
|
|
||||||
.map_err(|_| ())?;
|
|
||||||
stream.write_all(&raw).await.map_err(|_| ())?;
|
|
||||||
Ok(token)
|
Ok(token)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Wait for a status response.
|
/// Wait for a status response.
|
||||||
async fn wait_for_status(stream: &mut TcpStream) -> Result<ServerStatus, ()> {
|
async fn wait_for_status(client: &Client, stream: &mut TcpStream) -> Result<ServerStatus, ()> {
|
||||||
// Get stream reader, set up buffer
|
// Get stream reader, set up buffer
|
||||||
let (mut reader, mut _writer) = stream.split();
|
let (mut reader, mut _writer) = stream.split();
|
||||||
let mut buf = BytesMut::new();
|
let mut buf = BytesMut::new();
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
// Read packet from stream
|
// Read packet from stream
|
||||||
let (packet, _raw) = match proto::read_packet(&mut buf, &mut reader).await {
|
let (packet, _raw) = match packet::read_packet(client, &mut buf, &mut reader).await {
|
||||||
Ok(Some(packet)) => packet,
|
Ok(Some(packet)) => packet,
|
||||||
Ok(None) => break,
|
Ok(None) => break,
|
||||||
Err(_) => continue,
|
Err(_) => continue,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Catch status response
|
// Catch status response
|
||||||
if packet.id == proto::STATUS_PACKET_ID_STATUS {
|
if packet.id == packets::status::CLIENT_STATUS {
|
||||||
let status = StatusResponse::decode(&mut packet.data.as_slice()).map_err(|_| ())?;
|
let status = StatusResponse::decode(&mut packet.data.as_slice()).map_err(|_| ())?;
|
||||||
return Ok(status.server_status);
|
return Ok(status.server_status);
|
||||||
}
|
}
|
||||||
@@ -184,29 +194,32 @@ async fn wait_for_status(stream: &mut TcpStream) -> Result<ServerStatus, ()> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Wait for a status response.
|
/// Wait for a status response.
|
||||||
async fn wait_for_status_timeout(stream: &mut TcpStream) -> Result<ServerStatus, ()> {
|
async fn wait_for_status_timeout(
|
||||||
let status = wait_for_status(stream);
|
client: &Client,
|
||||||
|
stream: &mut TcpStream,
|
||||||
|
) -> Result<ServerStatus, ()> {
|
||||||
|
let status = wait_for_status(client, stream);
|
||||||
tokio::time::timeout(Duration::from_secs(STATUS_TIMEOUT), status)
|
tokio::time::timeout(Duration::from_secs(STATUS_TIMEOUT), status)
|
||||||
.await
|
.await
|
||||||
.map_err(|_| ())?
|
.map_err(|_| ())?
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Wait for a status response.
|
/// Wait for a status response.
|
||||||
async fn wait_for_ping(stream: &mut TcpStream, token: u64) -> Result<(), ()> {
|
async fn wait_for_ping(client: &Client, stream: &mut TcpStream, token: u64) -> Result<(), ()> {
|
||||||
// Get stream reader, set up buffer
|
// Get stream reader, set up buffer
|
||||||
let (mut reader, mut _writer) = stream.split();
|
let (mut reader, mut _writer) = stream.split();
|
||||||
let mut buf = BytesMut::new();
|
let mut buf = BytesMut::new();
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
// Read packet from stream
|
// Read packet from stream
|
||||||
let (packet, _raw) = match proto::read_packet(&mut buf, &mut reader).await {
|
let (packet, _raw) = match packet::read_packet(client, &mut buf, &mut reader).await {
|
||||||
Ok(Some(packet)) => packet,
|
Ok(Some(packet)) => packet,
|
||||||
Ok(None) => break,
|
Ok(None) => break,
|
||||||
Err(_) => continue,
|
Err(_) => continue,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Catch ping response
|
// Catch ping response
|
||||||
if packet.id == proto::STATUS_PACKET_ID_PING {
|
if packet.id == packets::status::CLIENT_PING {
|
||||||
let ping = PingResponse::decode(&mut packet.data.as_slice()).map_err(|_| ())?;
|
let ping = PingResponse::decode(&mut packet.data.as_slice()).map_err(|_| ())?;
|
||||||
|
|
||||||
// Ping token must match
|
// Ping token must match
|
||||||
@@ -223,8 +236,12 @@ async fn wait_for_ping(stream: &mut TcpStream, token: u64) -> Result<(), ()> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Wait for a status response.
|
/// Wait for a status response.
|
||||||
async fn wait_for_ping_timeout(stream: &mut TcpStream, token: u64) -> Result<(), ()> {
|
async fn wait_for_ping_timeout(
|
||||||
let status = wait_for_ping(stream, token);
|
client: &Client,
|
||||||
|
stream: &mut TcpStream,
|
||||||
|
token: u64,
|
||||||
|
) -> Result<(), ()> {
|
||||||
|
let status = wait_for_ping(client, stream, token);
|
||||||
tokio::time::timeout(Duration::from_secs(PING_TIMEOUT), status)
|
tokio::time::timeout(Duration::from_secs(PING_TIMEOUT), status)
|
||||||
.await
|
.await
|
||||||
.map_err(|_| ())?
|
.map_err(|_| ())?
|
||||||
|
22
src/net.rs
Normal file
22
src/net.rs
Normal 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()),
|
||||||
|
}
|
||||||
|
}
|
@@ -1,17 +1,16 @@
|
|||||||
#[cfg(unix)]
|
|
||||||
pub mod unix;
|
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
pub mod windows;
|
pub mod windows;
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
use nix::{sys::signal, unistd::Pid};
|
||||||
|
|
||||||
/// Force kill process.
|
/// Force kill process.
|
||||||
///
|
///
|
||||||
/// Results in undefined behavior if PID is invalid.
|
/// Results in undefined behavior if PID is invalid.
|
||||||
#[allow(unreachable_code)]
|
#[allow(unreachable_code)]
|
||||||
pub fn force_kill(pid: u32) -> bool {
|
pub fn force_kill(pid: u32) -> bool {
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
unsafe {
|
return unix_signal(pid, signal::SIGKILL);
|
||||||
return unix::force_kill(pid);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
unsafe {
|
unsafe {
|
||||||
@@ -22,20 +21,57 @@ pub fn force_kill(pid: u32) -> bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Gracefully kill process.
|
/// Gracefully kill process.
|
||||||
///
|
|
||||||
/// Results in undefined behavior if PID is invalid.
|
/// Results in undefined behavior if PID is invalid.
|
||||||
///
|
///
|
||||||
/// # Panics
|
/// # Panics
|
||||||
///
|
|
||||||
/// Panics on platforms other than Unix.
|
/// Panics on platforms other than Unix.
|
||||||
#[allow(unreachable_code, dead_code, unused_variables)]
|
#[allow(unreachable_code, dead_code, unused_variables)]
|
||||||
pub fn kill_gracefully(pid: u32) -> bool {
|
pub fn kill_gracefully(pid: u32) -> bool {
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
unsafe {
|
return unix_signal(pid, signal::SIGTERM);
|
||||||
return unix::kill_gracefully(pid);
|
|
||||||
}
|
|
||||||
|
|
||||||
unimplemented!(
|
unimplemented!(
|
||||||
"gracefully killing Minecraft server process not implemented on non-Unix platforms"
|
"gracefully killing Minecraft server process not implemented on non-Unix platforms"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Freeze process.
|
||||||
|
/// Results in undefined behavior if PID is invaild.
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
/// Panics on platforms other than Unix.
|
||||||
|
#[allow(unreachable_code)]
|
||||||
|
pub fn freeze(pid: u32) -> bool {
|
||||||
|
#[cfg(unix)]
|
||||||
|
return unix_signal(pid, signal::SIGSTOP);
|
||||||
|
|
||||||
|
unimplemented!(
|
||||||
|
"freezing the Minecraft server process is not implemented on non-Unix platforms"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Unfreeze process.
|
||||||
|
/// Results in undefined behavior if PID is invaild.
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
/// Panics on platforms other than Unix.
|
||||||
|
#[allow(unreachable_code)]
|
||||||
|
pub fn unfreeze(pid: u32) -> bool {
|
||||||
|
#[cfg(unix)]
|
||||||
|
return unix_signal(pid, signal::SIGCONT);
|
||||||
|
|
||||||
|
unimplemented!(
|
||||||
|
"unfreezing the Minecraft server process is not implemented on non-Unix platforms"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
pub fn unix_signal(pid: u32, signal: signal::Signal) -> bool {
|
||||||
|
return match signal::kill(Pid::from_raw(pid as i32), signal) {
|
||||||
|
Ok(()) => true,
|
||||||
|
Err(err) => {
|
||||||
|
warn!(target: "lazymc", "Sending {signal} signal to server failed: {err}");
|
||||||
|
false
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
@@ -1,27 +0,0 @@
|
|||||||
/// Force kill process on Unix by sending SIGKILL.
|
|
||||||
///
|
|
||||||
/// This is unsafe because the PID isn't checked.
|
|
||||||
pub unsafe fn force_kill(pid: u32) -> bool {
|
|
||||||
debug!(target: "lazymc", "Sending SIGKILL signal to {} to kill server", pid);
|
|
||||||
let result = libc::kill(pid as i32, libc::SIGKILL);
|
|
||||||
|
|
||||||
if result != 0 {
|
|
||||||
trace!(target: "lazymc", "SIGKILL failed: {}", result);
|
|
||||||
}
|
|
||||||
|
|
||||||
result == 0
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Gracefully kill process on Unix by sending SIGTERM.
|
|
||||||
///
|
|
||||||
/// This is unsafe because the PID isn't checked.
|
|
||||||
pub unsafe fn kill_gracefully(pid: u32) -> bool {
|
|
||||||
debug!(target: "lazymc", "Sending SIGTERM signal to {} to kill server", pid);
|
|
||||||
let result = libc::kill(pid as i32, libc::SIGTERM);
|
|
||||||
|
|
||||||
if result != 0 {
|
|
||||||
warn!(target: "lazymc", "Sending SIGTERM signal to server failed: {}", result);
|
|
||||||
}
|
|
||||||
|
|
||||||
result == 0
|
|
||||||
}
|
|
362
src/probe.rs
Normal file
362
src/probe.rs
Normal file
@@ -0,0 +1,362 @@
|
|||||||
|
use std::ops::Deref;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use bytes::BytesMut;
|
||||||
|
use minecraft_protocol::decoder::Decoder;
|
||||||
|
use minecraft_protocol::version::v1_14_4::handshake::Handshake;
|
||||||
|
use minecraft_protocol::version::v1_14_4::login::{
|
||||||
|
LoginPluginRequest, LoginPluginResponse, LoginStart, SetCompression,
|
||||||
|
};
|
||||||
|
use tokio::net::TcpStream;
|
||||||
|
use tokio::time;
|
||||||
|
|
||||||
|
use crate::config::Config;
|
||||||
|
use crate::forge;
|
||||||
|
use crate::net;
|
||||||
|
use crate::proto::client::{Client, ClientInfo, ClientState};
|
||||||
|
use crate::proto::packets::play::join_game::JoinGameData;
|
||||||
|
use crate::proto::{self, packet, packets};
|
||||||
|
use crate::server::{Server, State};
|
||||||
|
|
||||||
|
/// Minecraft username to use for probing the server.
|
||||||
|
const PROBE_USER: &str = "_lazymc_probe";
|
||||||
|
|
||||||
|
/// Timeout for probe user connecting to the server.
|
||||||
|
const PROBE_CONNECT_TIMEOUT: Duration = Duration::from_secs(30);
|
||||||
|
|
||||||
|
/// Maximum time the probe may wait for the server to come online.
|
||||||
|
const PROBE_ONLINE_TIMEOUT: Duration = Duration::from_secs(10 * 60);
|
||||||
|
|
||||||
|
/// Timeout for receiving join game packet.
|
||||||
|
///
|
||||||
|
/// When the play state is reached, the server should immeditely respond with a join game packet.
|
||||||
|
/// This defines the maximum timeout for waiting on it.
|
||||||
|
const PROBE_JOIN_GAME_TIMEOUT: Duration = Duration::from_secs(20);
|
||||||
|
|
||||||
|
/// Connect to the Minecraft server and probe useful details from it.
|
||||||
|
pub async fn probe(config: Arc<Config>, server: Arc<Server>) -> Result<(), ()> {
|
||||||
|
debug!(target: "lazymc::probe", "Starting server probe...");
|
||||||
|
|
||||||
|
// Start server if not starting already
|
||||||
|
if Server::start(config.clone(), server.clone(), None).await {
|
||||||
|
info!(target: "lazymc::probe", "Starting server to probe...");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for server to come online
|
||||||
|
if !wait_until_online(&server).await? {
|
||||||
|
warn!(target: "lazymc::probe", "Couldn't probe server, failed to wait for server to come online");
|
||||||
|
return Err(());
|
||||||
|
}
|
||||||
|
|
||||||
|
debug!(target: "lazymc::probe", "Connecting to server to probe details...");
|
||||||
|
|
||||||
|
// Connect to server, record Forge payload
|
||||||
|
let forge_payload = connect_to_server(&config, &server).await?;
|
||||||
|
*server.forge_payload.write().await = forge_payload;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Wait for the server to come online.
|
||||||
|
///
|
||||||
|
/// Returns `true` when it is online.
|
||||||
|
async fn wait_until_online<'a>(server: &Server) -> Result<bool, ()> {
|
||||||
|
trace!(target: "lazymc::probe", "Waiting for server to come online...");
|
||||||
|
|
||||||
|
// 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 => {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Server started, start relaying and proxy
|
||||||
|
State::Started => {
|
||||||
|
break true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Server stopping, this shouldn't happen, skip
|
||||||
|
State::Stopping => {
|
||||||
|
warn!(target: "lazymc::probe", "Server stopping while trying to probe, skipping");
|
||||||
|
break false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Server stopped, this shouldn't happen, skip
|
||||||
|
State::Stopped => {
|
||||||
|
error!(target: "lazymc::probe", "Server stopped while trying to probe, skipping");
|
||||||
|
break false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Wait for server state with timeout
|
||||||
|
match time::timeout(PROBE_ONLINE_TIMEOUT, task_wait).await {
|
||||||
|
Ok(online) => Ok(online),
|
||||||
|
|
||||||
|
// Timeout reached, kick with starting message
|
||||||
|
Err(_) => {
|
||||||
|
warn!(target: "lazymc::probe", "Probe waited for server to come online but timed out after {}s", PROBE_ONLINE_TIMEOUT.as_secs());
|
||||||
|
Ok(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create connection to the server, with timeout.
|
||||||
|
///
|
||||||
|
/// This will initialize the connection to the play state. Client details are used.
|
||||||
|
///
|
||||||
|
/// Returns recorded Forge login payload if any.
|
||||||
|
async fn connect_to_server(config: &Config, server: &Server) -> Result<Vec<Vec<u8>>, ()> {
|
||||||
|
time::timeout(
|
||||||
|
PROBE_CONNECT_TIMEOUT,
|
||||||
|
connect_to_server_no_timeout(config, server),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(|_| {
|
||||||
|
error!(target: "lazymc::probe", "Probe tried to connect to server but timed out after {}s", PROBE_CONNECT_TIMEOUT.as_secs());
|
||||||
|
})?
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create connection to the server, with no timeout.
|
||||||
|
///
|
||||||
|
/// This will initialize the connection to the play state. Client details are used.
|
||||||
|
///
|
||||||
|
/// Returns recorded Forge login payload if any.
|
||||||
|
// TODO: clean this up
|
||||||
|
async fn connect_to_server_no_timeout(
|
||||||
|
config: &Config,
|
||||||
|
server: &Server,
|
||||||
|
) -> Result<Vec<Vec<u8>>, ()> {
|
||||||
|
// Open connection
|
||||||
|
// TODO: on connect fail, ping server and redirect to serve_status if offline
|
||||||
|
let mut outbound = TcpStream::connect(config.server.address)
|
||||||
|
.await
|
||||||
|
.map_err(|_| ())?;
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
|
||||||
|
// Construct client info
|
||||||
|
let mut tmp_client_info = ClientInfo::empty();
|
||||||
|
tmp_client_info.protocol.replace(config.public.protocol);
|
||||||
|
|
||||||
|
let (mut reader, mut writer) = outbound.split();
|
||||||
|
|
||||||
|
// Select server address to use, add magic if Forge
|
||||||
|
let server_addr = if config.server.forge {
|
||||||
|
format!("{}{}", config.server.address.ip(), forge::STATUS_MAGIC)
|
||||||
|
} else {
|
||||||
|
config.server.address.ip().to_string()
|
||||||
|
};
|
||||||
|
|
||||||
|
// Send handshake packet
|
||||||
|
packet::write_packet(
|
||||||
|
Handshake {
|
||||||
|
protocol_version: config.public.protocol as i32,
|
||||||
|
server_addr,
|
||||||
|
server_port: config.server.address.port(),
|
||||||
|
next_state: ClientState::Login.to_id(),
|
||||||
|
},
|
||||||
|
&tmp_client,
|
||||||
|
&mut writer,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// Request login start
|
||||||
|
packet::write_packet(
|
||||||
|
LoginStart {
|
||||||
|
name: PROBE_USER.into(),
|
||||||
|
},
|
||||||
|
&tmp_client,
|
||||||
|
&mut writer,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// Incoming buffer, record Forge plugin request payload
|
||||||
|
let mut buf = BytesMut::new();
|
||||||
|
let mut forge_payload = Vec::new();
|
||||||
|
|
||||||
|
loop {
|
||||||
|
// Read packet from stream
|
||||||
|
let (packet, raw) = match packet::read_packet(&tmp_client, &mut buf, &mut reader).await {
|
||||||
|
Ok(Some(packet)) => packet,
|
||||||
|
Ok(None) => break,
|
||||||
|
Err(_) => {
|
||||||
|
error!(target: "lazymc::forge", "Closing connection, error occurred");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Grab client state
|
||||||
|
let client_state = tmp_client.state();
|
||||||
|
|
||||||
|
// Catch set compression
|
||||||
|
if client_state == ClientState::Login && packet.id == packets::login::CLIENT_SET_COMPRESSION
|
||||||
|
{
|
||||||
|
// Decode compression packet
|
||||||
|
let set_compression =
|
||||||
|
SetCompression::decode(&mut packet.data.as_slice()).map_err(|_| ())?;
|
||||||
|
|
||||||
|
// Client and server compression threshold should match, show warning if not
|
||||||
|
if set_compression.threshold != proto::COMPRESSION_THRESHOLD {
|
||||||
|
error!(
|
||||||
|
target: "lazymc::forge",
|
||||||
|
"Compression threshold sent to lobby client does not match threshold from server, this may cause errors (client: {}, server: {})",
|
||||||
|
proto::COMPRESSION_THRESHOLD,
|
||||||
|
set_compression.threshold
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set client compression
|
||||||
|
tmp_client.set_compression(set_compression.threshold);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Catch login plugin request
|
||||||
|
if client_state == ClientState::Login
|
||||||
|
&& packet.id == packets::login::CLIENT_LOGIN_PLUGIN_REQUEST
|
||||||
|
{
|
||||||
|
// Decode login plugin request packet
|
||||||
|
let plugin_request = LoginPluginRequest::decode(&mut packet.data.as_slice()).map_err(|err| {
|
||||||
|
error!(target: "lazymc::probe", "Failed to decode login plugin request from server, cannot respond properly: {:?}", err);
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// Handle plugin requests for Forge
|
||||||
|
if config.server.forge {
|
||||||
|
// Record Forge login payload
|
||||||
|
forge_payload.push(raw);
|
||||||
|
|
||||||
|
// Respond to Forge login plugin request
|
||||||
|
forge::respond_login_plugin_request(&tmp_client, plugin_request, &mut writer)
|
||||||
|
.await?;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
warn!(target: "lazymc::probe", "Got unexpected login plugin request, responding with error");
|
||||||
|
|
||||||
|
// Respond with plugin response failure
|
||||||
|
packet::write_packet(
|
||||||
|
LoginPluginResponse {
|
||||||
|
message_id: plugin_request.message_id,
|
||||||
|
successful: false,
|
||||||
|
data: vec![],
|
||||||
|
},
|
||||||
|
&tmp_client,
|
||||||
|
&mut writer,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hijack login success
|
||||||
|
if client_state == ClientState::Login && packet.id == packets::login::CLIENT_LOGIN_SUCCESS {
|
||||||
|
trace!(target: "lazymc::probe", "Got login success from server connection, change to play mode");
|
||||||
|
|
||||||
|
// Switch to play state
|
||||||
|
tmp_client.set_state(ClientState::Play);
|
||||||
|
|
||||||
|
// Wait to catch join game packet
|
||||||
|
let join_game_data =
|
||||||
|
wait_for_server_join_game(&tmp_client, &tmp_client_info, &mut outbound, &mut buf)
|
||||||
|
.await?;
|
||||||
|
server
|
||||||
|
.probed_join_game
|
||||||
|
.write()
|
||||||
|
.await
|
||||||
|
.replace(join_game_data);
|
||||||
|
|
||||||
|
// Gracefully close connection
|
||||||
|
let _ = net::close_tcp_stream(outbound).await;
|
||||||
|
|
||||||
|
return Ok(forge_payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show unhandled packet warning
|
||||||
|
debug!(target: "lazymc::forge", "Got unhandled packet from server in connect_to_server:");
|
||||||
|
debug!(target: "lazymc::forge", "- State: {:?}", client_state);
|
||||||
|
debug!(target: "lazymc::forge", "- Packet ID: 0x{:02X} ({})", packet.id, packet.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gracefully close connection
|
||||||
|
net::close_tcp_stream(outbound).await.map_err(|_| ())?;
|
||||||
|
|
||||||
|
Err(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Wait for join game packet on server connection, with timeout.
|
||||||
|
///
|
||||||
|
/// This parses, consumes and returns the packet.
|
||||||
|
async fn wait_for_server_join_game(
|
||||||
|
client: &Client,
|
||||||
|
client_info: &ClientInfo,
|
||||||
|
outbound: &mut TcpStream,
|
||||||
|
buf: &mut BytesMut,
|
||||||
|
) -> Result<JoinGameData, ()> {
|
||||||
|
time::timeout(
|
||||||
|
PROBE_JOIN_GAME_TIMEOUT,
|
||||||
|
wait_for_server_join_game_no_timeout(client, client_info, outbound, buf),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(|_| {
|
||||||
|
error!(target: "lazymc::probe", "Waiting for for game data from server for probe client timed out after {}s", PROBE_JOIN_GAME_TIMEOUT.as_secs());
|
||||||
|
})?
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Wait for join game packet on server connection, with no timeout.
|
||||||
|
///
|
||||||
|
/// This parses, consumes and returns the packet.
|
||||||
|
// TODO: clean this up
|
||||||
|
// TODO: do not drop error here, return Box<dyn Error>
|
||||||
|
async fn wait_for_server_join_game_no_timeout(
|
||||||
|
client: &Client,
|
||||||
|
client_info: &ClientInfo,
|
||||||
|
outbound: &mut TcpStream,
|
||||||
|
buf: &mut BytesMut,
|
||||||
|
) -> Result<JoinGameData, ()> {
|
||||||
|
let (mut reader, mut _writer) = outbound.split();
|
||||||
|
|
||||||
|
loop {
|
||||||
|
// Read packet from stream
|
||||||
|
let (packet, _raw) = match packet::read_packet(client, buf, &mut reader).await {
|
||||||
|
Ok(Some(packet)) => packet,
|
||||||
|
Ok(None) => break,
|
||||||
|
Err(_) => {
|
||||||
|
error!(target: "lazymc::probe", "Closing connection, error occurred");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Catch join game
|
||||||
|
if packets::play::join_game::is_packet(client_info, packet.id) {
|
||||||
|
// Parse join game data
|
||||||
|
let join_game_data = JoinGameData::from_packet(client_info, packet).map_err(|err| {
|
||||||
|
warn!(target: "lazymc::probe", "Failed to parse join game packet: {:?}", err);
|
||||||
|
})?;
|
||||||
|
|
||||||
|
return Ok(join_game_data);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show unhandled packet warning
|
||||||
|
debug!(target: "lazymc::probe", "Got unhandled packet from server in wait_for_server_join_game:");
|
||||||
|
debug!(target: "lazymc::probe", "- Packet ID: 0x{:02X} ({})", packet.id, packet.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gracefully close connection
|
||||||
|
net::close_tcp_stream_ref(outbound).await.map_err(|_| ())?;
|
||||||
|
|
||||||
|
Err(())
|
||||||
|
}
|
203
src/proto.rs
203
src/proto.rs
@@ -1,203 +0,0 @@
|
|||||||
use std::sync::Mutex;
|
|
||||||
|
|
||||||
use bytes::BytesMut;
|
|
||||||
use tokio::io;
|
|
||||||
use tokio::io::AsyncReadExt;
|
|
||||||
use tokio::net::tcp::ReadHalf;
|
|
||||||
|
|
||||||
use crate::types;
|
|
||||||
|
|
||||||
/// Default minecraft protocol version name.
|
|
||||||
///
|
|
||||||
/// Send to clients when the server is sleeping when the real server version is not known yet.
|
|
||||||
pub const PROTO_DEFAULT_VERSION: &str = "1.17.1";
|
|
||||||
|
|
||||||
/// Default minecraft protocol version.
|
|
||||||
///
|
|
||||||
/// Send to clients when the server is sleeping when the real server version is not known yet, and
|
|
||||||
/// with server status polling requests.
|
|
||||||
pub const PROTO_DEFAULT_PROTOCOL: u32 = 756;
|
|
||||||
|
|
||||||
/// Handshake state, handshake packet ID.
|
|
||||||
pub const HANDSHAKE_PACKET_ID_HANDSHAKE: i32 = 0;
|
|
||||||
|
|
||||||
/// Status state, status packet ID.
|
|
||||||
pub const STATUS_PACKET_ID_STATUS: i32 = 0;
|
|
||||||
|
|
||||||
/// Status state, ping packet ID.
|
|
||||||
pub const STATUS_PACKET_ID_PING: i32 = 1;
|
|
||||||
|
|
||||||
/// Login state, login start packet ID.
|
|
||||||
pub const LOGIN_PACKET_ID_LOGIN_START: i32 = 0;
|
|
||||||
|
|
||||||
/// Client state.
|
|
||||||
///
|
|
||||||
/// Note: this does not keep track of compression/encryption states because packets are never
|
|
||||||
/// inspected when these modes are enabled.
|
|
||||||
#[derive(Debug, Default)]
|
|
||||||
pub struct Client {
|
|
||||||
/// Current client state.
|
|
||||||
pub state: Mutex<ClientState>,
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 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,
|
|
||||||
}
|
|
||||||
|
|
||||||
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,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for ClientState {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::Handshake
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Raw Minecraft packet.
|
|
||||||
///
|
|
||||||
/// Having a packet ID and a raw data byte array.
|
|
||||||
pub struct RawPacket {
|
|
||||||
/// Packet ID.
|
|
||||||
pub id: i32,
|
|
||||||
|
|
||||||
/// Packet data.
|
|
||||||
pub data: Vec<u8>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RawPacket {
|
|
||||||
/// Construct new raw packet.
|
|
||||||
pub fn new(id: i32, data: Vec<u8>) -> Self {
|
|
||||||
Self { id, data }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Decode packet from raw buffer.
|
|
||||||
pub fn decode(mut buf: &[u8]) -> Result<Self, ()> {
|
|
||||||
// Read length
|
|
||||||
let (read, len) = types::read_var_int(buf)?;
|
|
||||||
buf = &buf[read..][..len as usize];
|
|
||||||
|
|
||||||
// Read packet ID, select buf
|
|
||||||
let (read, packet_id) = types::read_var_int(buf)?;
|
|
||||||
buf = &buf[read..];
|
|
||||||
|
|
||||||
Ok(Self::new(packet_id, buf.to_vec()))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Encode packet to raw buffer.
|
|
||||||
pub fn encode(&self) -> Result<Vec<u8>, ()> {
|
|
||||||
let mut data = types::encode_var_int(self.id)?;
|
|
||||||
data.extend_from_slice(&self.data);
|
|
||||||
|
|
||||||
let len = data.len() as i32;
|
|
||||||
let mut packet = types::encode_var_int(len)?;
|
|
||||||
packet.append(&mut data);
|
|
||||||
|
|
||||||
Ok(packet)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Read raw packet from stream.
|
|
||||||
///
|
|
||||||
/// Note: this does not support reading compressed/encrypted packets.
|
|
||||||
/// We should never need this though, as we're done reading user packets before any of this is
|
|
||||||
/// enabled. See: https://wiki.vg/Protocol#Packet_format
|
|
||||||
pub async fn read_packet(
|
|
||||||
buf: &mut BytesMut,
|
|
||||||
stream: &mut ReadHalf<'_>,
|
|
||||||
) -> Result<Option<(RawPacket, Vec<u8>)>, ()> {
|
|
||||||
// Keep reading until we have at least 2 bytes
|
|
||||||
while buf.len() < 2 {
|
|
||||||
// Read packet from socket
|
|
||||||
let mut tmp = Vec::with_capacity(64);
|
|
||||||
match stream.read_buf(&mut tmp).await {
|
|
||||||
Ok(_) => {}
|
|
||||||
Err(err) if err.kind() == io::ErrorKind::ConnectionReset => return Ok(None),
|
|
||||||
Err(err) => {
|
|
||||||
dbg!(err);
|
|
||||||
return Err(());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if tmp.is_empty() {
|
|
||||||
return Ok(None);
|
|
||||||
}
|
|
||||||
buf.extend(tmp);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Attempt to read packet length
|
|
||||||
let (consumed, len) = match types::read_var_int(buf) {
|
|
||||||
Ok(result) => result,
|
|
||||||
Err(err) => {
|
|
||||||
error!(target: "lazymc", "Malformed packet, could not read packet length");
|
|
||||||
return Err(err);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Keep reading until we have all packet bytes
|
|
||||||
while buf.len() < consumed + len as usize {
|
|
||||||
// Read packet from socket
|
|
||||||
let mut tmp = Vec::with_capacity(64);
|
|
||||||
match stream.read_buf(&mut tmp).await {
|
|
||||||
Ok(_) => {}
|
|
||||||
Err(err) if err.kind() == io::ErrorKind::ConnectionReset => return Ok(None),
|
|
||||||
Err(err) => {
|
|
||||||
dbg!(err);
|
|
||||||
return Err(());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if tmp.is_empty() {
|
|
||||||
return Ok(None);
|
|
||||||
}
|
|
||||||
|
|
||||||
buf.extend(tmp);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse packet
|
|
||||||
let raw = buf.split_to(consumed + len as usize);
|
|
||||||
let packet = RawPacket::decode(&raw)?;
|
|
||||||
|
|
||||||
Ok(Some((packet, raw.to_vec())))
|
|
||||||
}
|
|
36
src/proto/action.rs
Normal file
36
src/proto/action.rs
Normal 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(()),
|
||||||
|
}
|
||||||
|
}
|
138
src/proto/client.rs
Normal file
138
src/proto/client.rs
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
use std::net::SocketAddr;
|
||||||
|
use std::sync::atomic::{AtomicI32, Ordering};
|
||||||
|
use std::sync::Mutex;
|
||||||
|
|
||||||
|
use minecraft_protocol::version::v1_14_4::handshake::Handshake;
|
||||||
|
|
||||||
|
/// 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 {
|
||||||
|
/// Used protocol version.
|
||||||
|
pub protocol: Option<u32>,
|
||||||
|
|
||||||
|
/// Handshake as received from client.
|
||||||
|
pub handshake: Option<Handshake>,
|
||||||
|
|
||||||
|
/// Client username.
|
||||||
|
pub username: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ClientInfo {
|
||||||
|
pub fn empty() -> Self {
|
||||||
|
Self::default()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get protocol version.
|
||||||
|
pub fn protocol(&self) -> Option<u32> {
|
||||||
|
self.protocol
|
||||||
|
.or_else(|| self.handshake.as_ref().map(|h| h.protocol_version as u32))
|
||||||
|
}
|
||||||
|
}
|
27
src/proto/mod.rs
Normal file
27
src/proto/mod.rs
Normal 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.19.3";
|
||||||
|
|
||||||
|
/// 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 = 761;
|
||||||
|
|
||||||
|
/// 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;
|
235
src/proto/packet.rs
Normal file
235
src/proto/packet.rs
Normal file
@@ -0,0 +1,235 @@
|
|||||||
|
use std::fmt::Debug;
|
||||||
|
use std::io::prelude::*;
|
||||||
|
|
||||||
|
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, AsyncWriteExt};
|
||||||
|
use tokio::net::tcp::{ReadHalf, WriteHalf};
|
||||||
|
|
||||||
|
use crate::proto::client::Client;
|
||||||
|
use crate::proto::BUF_SIZE;
|
||||||
|
use crate::types;
|
||||||
|
|
||||||
|
/// Raw Minecraft packet.
|
||||||
|
///
|
||||||
|
/// Having a packet ID and a raw data byte array.
|
||||||
|
pub struct RawPacket {
|
||||||
|
/// Packet ID.
|
||||||
|
pub id: u8,
|
||||||
|
|
||||||
|
/// Packet data.
|
||||||
|
pub data: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RawPacket {
|
||||||
|
/// Construct new raw packet.
|
||||||
|
pub fn new(id: u8, data: Vec<u8>) -> Self {
|
||||||
|
Self { id, data }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read packet ID from buffer, use remaining buffer as data.
|
||||||
|
fn read_packet_id_data(mut buf: &[u8]) -> Result<Self, ()> {
|
||||||
|
// Read packet ID, select buf
|
||||||
|
let (read, packet_id) = types::read_var_int(buf)?;
|
||||||
|
buf = &buf[read..];
|
||||||
|
|
||||||
|
Ok(Self::new(packet_id as u8, buf.to_vec()))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Decode packet from raw buffer.
|
||||||
|
///
|
||||||
|
/// This decodes both compressed and uncompressed packets based on the client threshold
|
||||||
|
/// preference.
|
||||||
|
pub fn decode_with_len(client: &Client, mut buf: &[u8]) -> Result<Self, ()> {
|
||||||
|
// Read length
|
||||||
|
let (read, len) = types::read_var_int(buf)?;
|
||||||
|
buf = &buf[read..][..len as usize];
|
||||||
|
|
||||||
|
// TODO: assert buffer length!
|
||||||
|
|
||||||
|
Self::decode_without_len(client, buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Decode packet from raw buffer without packet length.
|
||||||
|
///
|
||||||
|
/// This decodes both compressed and uncompressed packets based on the client threshold
|
||||||
|
/// preference.
|
||||||
|
/// The length is given, and not included in the buffer itself.
|
||||||
|
pub fn decode_without_len(client: &Client, mut buf: &[u8]) -> Result<Self, ()> {
|
||||||
|
// If no compression is used, read remaining packet ID and data
|
||||||
|
if !client.is_compressed() {
|
||||||
|
// Read packet ID and data
|
||||||
|
return Self::read_packet_id_data(buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read data length
|
||||||
|
let (read, data_len) = types::read_var_int(buf)?;
|
||||||
|
buf = &buf[read..];
|
||||||
|
|
||||||
|
// If data length is zero, the rest is not compressed
|
||||||
|
if data_len == 0 {
|
||||||
|
return Self::read_packet_id_data(buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decompress packet ID and data section
|
||||||
|
let mut decompressed = Vec::with_capacity(data_len as usize);
|
||||||
|
ZlibDecoder::new(buf)
|
||||||
|
.read_to_end(&mut decompressed)
|
||||||
|
.map_err(|err| {
|
||||||
|
error!(target: "lazymc", "Packet decompression error: {}", err);
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// Decompressed data must match length
|
||||||
|
if decompressed.len() != data_len as usize {
|
||||||
|
error!(target: "lazymc", "Decompressed packet has different length than expected ({}b != {}b)", decompressed.len(), data_len);
|
||||||
|
return Err(());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read decompressed packet ID
|
||||||
|
Self::read_packet_id_data(&decompressed)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Encode packet to raw buffer.
|
||||||
|
///
|
||||||
|
/// This compresses packets based on the client threshold preference.
|
||||||
|
pub fn encode_with_len(&self, client: &Client) -> Result<Vec<u8>, ()> {
|
||||||
|
// Encode packet without length
|
||||||
|
let mut payload = self.encode_without_len(client)?;
|
||||||
|
|
||||||
|
// Add length header
|
||||||
|
let mut packet = types::encode_var_int(payload.len() as i32)?;
|
||||||
|
packet.append(&mut payload);
|
||||||
|
Ok(packet)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Encode packet to raw buffer without length header.
|
||||||
|
///
|
||||||
|
/// This compresses packets based on the client threshold preference.
|
||||||
|
pub fn encode_without_len(&self, client: &Client) -> Result<Vec<u8>, ()> {
|
||||||
|
let threshold = client.compressed();
|
||||||
|
if threshold >= 0 {
|
||||||
|
self.encode_compressed(threshold)
|
||||||
|
} else {
|
||||||
|
self.encode_uncompressed()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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 as i32)?;
|
||||||
|
payload.extend_from_slice(&self.data);
|
||||||
|
|
||||||
|
// Determine whether to compress, encode data length bytes
|
||||||
|
let data_len = payload.len() as i32;
|
||||||
|
let compress = data_len > threshold;
|
||||||
|
let data_len_header = if compress { data_len } else { 0 };
|
||||||
|
|
||||||
|
// Compress payload
|
||||||
|
if compress {
|
||||||
|
let mut encoder = ZlibEncoder::new(Vec::new(), Compression::default());
|
||||||
|
encoder.write_all(&payload).map_err(|err| {
|
||||||
|
error!(target: "lazymc", "Failed to compress packet: {}", err);
|
||||||
|
})?;
|
||||||
|
payload = encoder.finish().map_err(|err| {
|
||||||
|
error!(target: "lazymc", "Failed to compress packet: {}", err);
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add data length header
|
||||||
|
let mut packet = types::encode_var_int(data_len_header).unwrap();
|
||||||
|
packet.append(&mut payload);
|
||||||
|
|
||||||
|
Ok(packet)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Encode uncompressed packet to raw buffer.
|
||||||
|
fn encode_uncompressed(&self) -> Result<Vec<u8>, ()> {
|
||||||
|
let mut packet = types::encode_var_int(self.id as i32)?;
|
||||||
|
packet.extend_from_slice(&self.data);
|
||||||
|
|
||||||
|
Ok(packet)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read raw packet from stream.
|
||||||
|
pub async fn read_packet(
|
||||||
|
client: &Client,
|
||||||
|
buf: &mut BytesMut,
|
||||||
|
stream: &mut ReadHalf<'_>,
|
||||||
|
) -> Result<Option<(RawPacket, Vec<u8>)>, ()> {
|
||||||
|
// Keep reading until we have at least 2 bytes
|
||||||
|
while buf.len() < 2 {
|
||||||
|
// Read packet from socket
|
||||||
|
let mut tmp = Vec::with_capacity(BUF_SIZE);
|
||||||
|
match stream.read_buf(&mut tmp).await {
|
||||||
|
Ok(_) => {}
|
||||||
|
Err(err) if err.kind() == io::ErrorKind::ConnectionReset => return Ok(None),
|
||||||
|
Err(err) => {
|
||||||
|
dbg!(err);
|
||||||
|
return Err(());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if tmp.is_empty() {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
buf.extend(tmp);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attempt to read packet length
|
||||||
|
let (consumed, len) = match types::read_var_int(buf) {
|
||||||
|
Ok(result) => result,
|
||||||
|
Err(err) => {
|
||||||
|
error!(target: "lazymc", "Malformed packet, could not read packet length");
|
||||||
|
return Err(err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Keep reading until we have all packet bytes
|
||||||
|
while buf.len() < consumed + len as usize {
|
||||||
|
// Read packet from socket
|
||||||
|
let mut tmp = Vec::with_capacity(BUF_SIZE);
|
||||||
|
match stream.read_buf(&mut tmp).await {
|
||||||
|
Ok(_) => {}
|
||||||
|
Err(err) if err.kind() == io::ErrorKind::ConnectionReset => return Ok(None),
|
||||||
|
Err(err) => {
|
||||||
|
dbg!(err);
|
||||||
|
return Err(());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if tmp.is_empty() {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
|
||||||
|
buf.extend(tmp);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse packet, use full buffer since we'll read the packet length again
|
||||||
|
// TODO: use decode_without_len, strip len from buffer
|
||||||
|
let raw = buf.split_to(consumed + len as usize);
|
||||||
|
let packet = RawPacket::decode_with_len(client, &raw)?;
|
||||||
|
|
||||||
|
Ok(Some((packet, raw.to_vec())))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Write packet to stream writer.
|
||||||
|
pub async fn write_packet(
|
||||||
|
packet: impl PacketId + Encoder + Debug,
|
||||||
|
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_with_len(client)?;
|
||||||
|
writer.write_all(&response).await.map_err(|_| ())?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
33
src/proto/packets/mod.rs
Normal file
33
src/proto/packets/mod.rs
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
//! Minecraft protocol packet IDs.
|
||||||
|
|
||||||
|
pub mod play;
|
||||||
|
|
||||||
|
pub mod handshake {
|
||||||
|
use minecraft_protocol::version::v1_14_4::handshake::*;
|
||||||
|
|
||||||
|
pub const SERVER_HANDSHAKE: u8 = Handshake::PACKET_ID;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub mod status {
|
||||||
|
use minecraft_protocol::version::v1_14_4::status::*;
|
||||||
|
|
||||||
|
pub const CLIENT_STATUS: u8 = StatusResponse::PACKET_ID;
|
||||||
|
pub const CLIENT_PING: u8 = PingResponse::PACKET_ID;
|
||||||
|
pub const SERVER_STATUS: u8 = StatusRequest::PACKET_ID;
|
||||||
|
pub const SERVER_PING: u8 = PingRequest::PACKET_ID;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub mod login {
|
||||||
|
use minecraft_protocol::version::v1_14_4::login::*;
|
||||||
|
|
||||||
|
#[cfg(feature = "lobby")]
|
||||||
|
pub const CLIENT_DISCONNECT: u8 = LoginDisconnect::PACKET_ID;
|
||||||
|
pub const CLIENT_LOGIN_SUCCESS: u8 = LoginSuccess::PACKET_ID;
|
||||||
|
pub const CLIENT_SET_COMPRESSION: u8 = SetCompression::PACKET_ID;
|
||||||
|
#[cfg(feature = "lobby")]
|
||||||
|
pub const CLIENT_ENCRYPTION_REQUEST: u8 = EncryptionRequest::PACKET_ID;
|
||||||
|
pub const CLIENT_LOGIN_PLUGIN_REQUEST: u8 = LoginPluginRequest::PACKET_ID;
|
||||||
|
pub const SERVER_LOGIN_START: u8 = LoginStart::PACKET_ID;
|
||||||
|
#[cfg(feature = "lobby")]
|
||||||
|
pub const SERVER_LOGIN_PLUGIN_RESPONSE: u8 = LoginPluginResponse::PACKET_ID;
|
||||||
|
}
|
208
src/proto/packets/play/join_game.rs
Normal file
208
src/proto/packets/play/join_game.rs
Normal file
@@ -0,0 +1,208 @@
|
|||||||
|
use minecraft_protocol::decoder::Decoder;
|
||||||
|
use minecraft_protocol::error::DecodeError;
|
||||||
|
use minecraft_protocol::version::{v1_16_3, v1_17};
|
||||||
|
use nbt::CompoundTag;
|
||||||
|
#[cfg(feature = "lobby")]
|
||||||
|
use tokio::net::tcp::WriteHalf;
|
||||||
|
|
||||||
|
#[cfg(feature = "lobby")]
|
||||||
|
use crate::mc::dimension;
|
||||||
|
#[cfg(feature = "lobby")]
|
||||||
|
use crate::proto::client::Client;
|
||||||
|
use crate::proto::client::ClientInfo;
|
||||||
|
#[cfg(feature = "lobby")]
|
||||||
|
use crate::proto::packet;
|
||||||
|
use crate::proto::packet::RawPacket;
|
||||||
|
#[cfg(feature = "lobby")]
|
||||||
|
use crate::server::Server;
|
||||||
|
|
||||||
|
/// Data extracted from `JoinGame` packet.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct JoinGameData {
|
||||||
|
pub hardcore: Option<bool>,
|
||||||
|
pub game_mode: Option<u8>,
|
||||||
|
pub previous_game_mode: Option<u8>,
|
||||||
|
pub world_names: Option<Vec<String>>,
|
||||||
|
pub dimension: Option<CompoundTag>,
|
||||||
|
pub dimension_codec: Option<CompoundTag>,
|
||||||
|
pub world_name: Option<String>,
|
||||||
|
pub hashed_seed: Option<i64>,
|
||||||
|
pub max_players: Option<i32>,
|
||||||
|
pub view_distance: Option<i32>,
|
||||||
|
pub reduced_debug_info: Option<bool>,
|
||||||
|
pub enable_respawn_screen: Option<bool>,
|
||||||
|
pub is_debug: Option<bool>,
|
||||||
|
pub is_flat: Option<bool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl JoinGameData {
|
||||||
|
/// Extract join game data from given packet.
|
||||||
|
pub fn from_packet(client_info: &ClientInfo, packet: RawPacket) -> Result<Self, DecodeError> {
|
||||||
|
match client_info.protocol() {
|
||||||
|
Some(p) if p < v1_17::PROTOCOL => {
|
||||||
|
Ok(v1_16_3::game::JoinGame::decode(&mut packet.data.as_slice())?.into())
|
||||||
|
}
|
||||||
|
_ => Ok(v1_17::game::JoinGame::decode(&mut packet.data.as_slice())?.into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<v1_16_3::game::JoinGame> for JoinGameData {
|
||||||
|
fn from(join_game: v1_16_3::game::JoinGame) -> Self {
|
||||||
|
Self {
|
||||||
|
hardcore: Some(join_game.hardcore),
|
||||||
|
game_mode: Some(join_game.game_mode),
|
||||||
|
previous_game_mode: Some(join_game.previous_game_mode),
|
||||||
|
world_names: Some(join_game.world_names.clone()),
|
||||||
|
dimension: Some(join_game.dimension),
|
||||||
|
dimension_codec: Some(join_game.dimension_codec),
|
||||||
|
world_name: Some(join_game.world_name),
|
||||||
|
hashed_seed: Some(join_game.hashed_seed),
|
||||||
|
max_players: Some(join_game.max_players),
|
||||||
|
view_distance: Some(join_game.view_distance),
|
||||||
|
reduced_debug_info: Some(join_game.reduced_debug_info),
|
||||||
|
enable_respawn_screen: Some(join_game.enable_respawn_screen),
|
||||||
|
is_debug: Some(join_game.is_debug),
|
||||||
|
is_flat: Some(join_game.is_flat),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<v1_17::game::JoinGame> for JoinGameData {
|
||||||
|
fn from(join_game: v1_17::game::JoinGame) -> Self {
|
||||||
|
Self {
|
||||||
|
hardcore: Some(join_game.hardcore),
|
||||||
|
game_mode: Some(join_game.game_mode),
|
||||||
|
previous_game_mode: Some(join_game.previous_game_mode),
|
||||||
|
world_names: Some(join_game.world_names.clone()),
|
||||||
|
dimension: Some(join_game.dimension),
|
||||||
|
dimension_codec: Some(join_game.dimension_codec),
|
||||||
|
world_name: Some(join_game.world_name),
|
||||||
|
hashed_seed: Some(join_game.hashed_seed),
|
||||||
|
max_players: Some(join_game.max_players),
|
||||||
|
view_distance: Some(join_game.view_distance),
|
||||||
|
reduced_debug_info: Some(join_game.reduced_debug_info),
|
||||||
|
enable_respawn_screen: Some(join_game.enable_respawn_screen),
|
||||||
|
is_debug: Some(join_game.is_debug),
|
||||||
|
is_flat: Some(join_game.is_flat),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check whether the packet ID matches.
|
||||||
|
pub fn is_packet(client_info: &ClientInfo, packet_id: u8) -> bool {
|
||||||
|
match client_info.protocol() {
|
||||||
|
Some(p) if p < v1_17::PROTOCOL => packet_id == v1_16_3::game::JoinGame::PACKET_ID,
|
||||||
|
_ => packet_id == v1_17::game::JoinGame::PACKET_ID,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Send initial join game packet to client for lobby.
|
||||||
|
#[cfg(feature = "lobby")]
|
||||||
|
pub async fn lobby_send(
|
||||||
|
client: &Client,
|
||||||
|
client_info: &ClientInfo,
|
||||||
|
writer: &mut WriteHalf<'_>,
|
||||||
|
server: &Server,
|
||||||
|
) -> Result<(), ()> {
|
||||||
|
let status = server.status().await;
|
||||||
|
let join_game = server.probed_join_game.read().await;
|
||||||
|
|
||||||
|
// Get dimension codec and build lobby dimension
|
||||||
|
let dimension_codec: CompoundTag = if let Some(join_game) = join_game.as_ref() {
|
||||||
|
join_game
|
||||||
|
.dimension_codec
|
||||||
|
.clone()
|
||||||
|
.unwrap_or_else(dimension::default_dimension_codec)
|
||||||
|
} else {
|
||||||
|
dimension::default_dimension_codec()
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get other values from status and probed join game data
|
||||||
|
let dimension: CompoundTag = dimension::lobby_dimension(&dimension_codec);
|
||||||
|
let hardcore = join_game.as_ref().and_then(|p| p.hardcore).unwrap_or(false);
|
||||||
|
let world_names = join_game
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|p| p.world_names.clone())
|
||||||
|
.unwrap_or_else(|| {
|
||||||
|
vec![
|
||||||
|
"minecraft:overworld".into(),
|
||||||
|
"minecraft:the_nether".into(),
|
||||||
|
"minecraft:the_end".into(),
|
||||||
|
]
|
||||||
|
});
|
||||||
|
let max_players = status
|
||||||
|
.as_ref()
|
||||||
|
.map(|s| s.players.max as i32)
|
||||||
|
.or_else(|| join_game.as_ref().and_then(|p| p.max_players))
|
||||||
|
.unwrap_or(20);
|
||||||
|
let view_distance = join_game
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|p| p.view_distance)
|
||||||
|
.unwrap_or(10);
|
||||||
|
let reduced_debug_info = join_game
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|p| p.reduced_debug_info)
|
||||||
|
.unwrap_or(false);
|
||||||
|
let enable_respawn_screen = join_game
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|p| p.enable_respawn_screen)
|
||||||
|
.unwrap_or(true);
|
||||||
|
let is_debug = join_game.as_ref().and_then(|p| p.is_debug).unwrap_or(false);
|
||||||
|
let is_flat = join_game.as_ref().and_then(|p| p.is_flat).unwrap_or(false);
|
||||||
|
|
||||||
|
match client_info.protocol() {
|
||||||
|
Some(p) if p < v1_17::PROTOCOL => {
|
||||||
|
packet::write_packet(
|
||||||
|
v1_16_3::game::JoinGame {
|
||||||
|
// Player ID must be unique, if it collides with another server entity ID the player gets
|
||||||
|
// in a weird state and cannot move
|
||||||
|
entity_id: 0,
|
||||||
|
hardcore,
|
||||||
|
game_mode: 3,
|
||||||
|
previous_game_mode: -1i8 as u8,
|
||||||
|
world_names,
|
||||||
|
dimension_codec,
|
||||||
|
dimension,
|
||||||
|
world_name: "lazymc:lobby".into(),
|
||||||
|
hashed_seed: 0,
|
||||||
|
max_players,
|
||||||
|
view_distance,
|
||||||
|
reduced_debug_info,
|
||||||
|
enable_respawn_screen,
|
||||||
|
is_debug,
|
||||||
|
is_flat,
|
||||||
|
},
|
||||||
|
client,
|
||||||
|
writer,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
packet::write_packet(
|
||||||
|
v1_17::game::JoinGame {
|
||||||
|
// Player ID must be unique, if it collides with another server entity ID the player gets
|
||||||
|
// in a weird state and cannot move
|
||||||
|
entity_id: 0,
|
||||||
|
hardcore,
|
||||||
|
game_mode: 3,
|
||||||
|
previous_game_mode: -1i8 as u8,
|
||||||
|
world_names,
|
||||||
|
dimension_codec,
|
||||||
|
dimension,
|
||||||
|
world_name: "lazymc:lobby".into(),
|
||||||
|
hashed_seed: 0,
|
||||||
|
max_players,
|
||||||
|
view_distance,
|
||||||
|
reduced_debug_info,
|
||||||
|
enable_respawn_screen,
|
||||||
|
is_debug,
|
||||||
|
is_flat,
|
||||||
|
},
|
||||||
|
client,
|
||||||
|
writer,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
29
src/proto/packets/play/keep_alive.rs
Normal file
29
src/proto/packets/play/keep_alive.rs
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
use std::sync::atomic::{AtomicU64, Ordering};
|
||||||
|
|
||||||
|
use minecraft_protocol::version::{v1_16_3, v1_17};
|
||||||
|
use tokio::net::tcp::WriteHalf;
|
||||||
|
|
||||||
|
use crate::proto::client::{Client, ClientInfo};
|
||||||
|
use crate::proto::packet;
|
||||||
|
|
||||||
|
/// Auto incrementing ID source for keep alive packets.
|
||||||
|
static KEEP_ALIVE_ID: AtomicU64 = AtomicU64::new(0);
|
||||||
|
|
||||||
|
/// Send keep alive packet to client.
|
||||||
|
///
|
||||||
|
/// Required periodically in play mode to prevent client timeout.
|
||||||
|
pub async fn send(
|
||||||
|
client: &Client,
|
||||||
|
client_info: &ClientInfo,
|
||||||
|
writer: &mut WriteHalf<'_>,
|
||||||
|
) -> Result<(), ()> {
|
||||||
|
// Keep sending new IDs
|
||||||
|
let id = KEEP_ALIVE_ID.fetch_add(1, Ordering::Relaxed);
|
||||||
|
|
||||||
|
match client_info.protocol() {
|
||||||
|
Some(p) if p < v1_17::PROTOCOL => {
|
||||||
|
packet::write_packet(v1_16_3::game::ClientBoundKeepAlive { id }, client, writer).await
|
||||||
|
}
|
||||||
|
_ => packet::write_packet(v1_17::game::ClientBoundKeepAlive { id }, client, writer).await,
|
||||||
|
}
|
||||||
|
}
|
15
src/proto/packets/play/mod.rs
Normal file
15
src/proto/packets/play/mod.rs
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
pub mod join_game;
|
||||||
|
#[cfg(feature = "lobby")]
|
||||||
|
pub mod keep_alive;
|
||||||
|
#[cfg(feature = "lobby")]
|
||||||
|
pub mod player_pos;
|
||||||
|
#[cfg(feature = "lobby")]
|
||||||
|
pub mod respawn;
|
||||||
|
#[cfg(feature = "lobby")]
|
||||||
|
pub mod server_brand;
|
||||||
|
#[cfg(feature = "lobby")]
|
||||||
|
pub mod sound;
|
||||||
|
#[cfg(feature = "lobby")]
|
||||||
|
pub mod time_update;
|
||||||
|
#[cfg(feature = "lobby")]
|
||||||
|
pub mod title;
|
48
src/proto/packets/play/player_pos.rs
Normal file
48
src/proto/packets/play/player_pos.rs
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
use minecraft_protocol::version::{v1_16_3, v1_17};
|
||||||
|
use tokio::net::tcp::WriteHalf;
|
||||||
|
|
||||||
|
use crate::proto::client::{Client, ClientInfo};
|
||||||
|
use crate::proto::packet;
|
||||||
|
|
||||||
|
/// Move player to world origin.
|
||||||
|
pub async fn send(
|
||||||
|
client: &Client,
|
||||||
|
client_info: &ClientInfo,
|
||||||
|
writer: &mut WriteHalf<'_>,
|
||||||
|
) -> Result<(), ()> {
|
||||||
|
match client_info.protocol() {
|
||||||
|
Some(p) if p < v1_17::PROTOCOL => {
|
||||||
|
packet::write_packet(
|
||||||
|
v1_16_3::game::PlayerPositionAndLook {
|
||||||
|
x: 0.0,
|
||||||
|
y: 0.0,
|
||||||
|
z: 0.0,
|
||||||
|
yaw: 0.0,
|
||||||
|
pitch: 90.0,
|
||||||
|
flags: 0b00000000,
|
||||||
|
teleport_id: 0,
|
||||||
|
},
|
||||||
|
client,
|
||||||
|
writer,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
packet::write_packet(
|
||||||
|
v1_17::game::PlayerPositionAndLook {
|
||||||
|
x: 0.0,
|
||||||
|
y: 0.0,
|
||||||
|
z: 0.0,
|
||||||
|
yaw: 0.0,
|
||||||
|
pitch: 90.0,
|
||||||
|
flags: 0b00000000,
|
||||||
|
teleport_id: 0,
|
||||||
|
dismount_vehicle: true,
|
||||||
|
},
|
||||||
|
client,
|
||||||
|
writer,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
70
src/proto/packets/play/respawn.rs
Normal file
70
src/proto/packets/play/respawn.rs
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
use minecraft_protocol::version::{v1_16_3, v1_17};
|
||||||
|
use tokio::net::tcp::WriteHalf;
|
||||||
|
|
||||||
|
use super::join_game::JoinGameData;
|
||||||
|
use crate::mc::dimension;
|
||||||
|
use crate::proto::client::{Client, ClientInfo};
|
||||||
|
use crate::proto::packet;
|
||||||
|
|
||||||
|
/// Send respawn packet to client to jump from lobby into now loaded server.
|
||||||
|
///
|
||||||
|
/// The required details will be fetched from the `join_game` packet as provided by the server.
|
||||||
|
pub async fn lobby_send(
|
||||||
|
client: &Client,
|
||||||
|
client_info: &ClientInfo,
|
||||||
|
writer: &mut WriteHalf<'_>,
|
||||||
|
data: JoinGameData,
|
||||||
|
) -> Result<(), ()> {
|
||||||
|
match client_info.protocol() {
|
||||||
|
Some(p) if p < v1_17::PROTOCOL => {
|
||||||
|
packet::write_packet(
|
||||||
|
v1_16_3::game::Respawn {
|
||||||
|
dimension: data.dimension.unwrap_or_else(|| {
|
||||||
|
dimension::lobby_dimension(
|
||||||
|
&data
|
||||||
|
.dimension_codec
|
||||||
|
.unwrap_or_else(dimension::default_dimension_codec),
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
world_name: data
|
||||||
|
.world_name
|
||||||
|
.unwrap_or_else(|| "minecraft:overworld".into()),
|
||||||
|
hashed_seed: data.hashed_seed.unwrap_or(0),
|
||||||
|
game_mode: data.game_mode.unwrap_or(0),
|
||||||
|
previous_game_mode: data.previous_game_mode.unwrap_or(-1i8 as u8),
|
||||||
|
is_debug: data.is_debug.unwrap_or(false),
|
||||||
|
is_flat: data.is_flat.unwrap_or(false),
|
||||||
|
copy_metadata: false,
|
||||||
|
},
|
||||||
|
client,
|
||||||
|
writer,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
packet::write_packet(
|
||||||
|
v1_17::game::Respawn {
|
||||||
|
dimension: data.dimension.unwrap_or_else(|| {
|
||||||
|
dimension::lobby_dimension(
|
||||||
|
&data
|
||||||
|
.dimension_codec
|
||||||
|
.unwrap_or_else(dimension::default_dimension_codec),
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
world_name: data
|
||||||
|
.world_name
|
||||||
|
.unwrap_or_else(|| "minecraft:overworld".into()),
|
||||||
|
hashed_seed: data.hashed_seed.unwrap_or(0),
|
||||||
|
game_mode: data.game_mode.unwrap_or(0),
|
||||||
|
previous_game_mode: data.previous_game_mode.unwrap_or(-1i8 as u8),
|
||||||
|
is_debug: data.is_debug.unwrap_or(false),
|
||||||
|
is_flat: data.is_flat.unwrap_or(false),
|
||||||
|
copy_metadata: false,
|
||||||
|
},
|
||||||
|
client,
|
||||||
|
writer,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
45
src/proto/packets/play/server_brand.rs
Normal file
45
src/proto/packets/play/server_brand.rs
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
use minecraft_protocol::version::{v1_16_3, v1_17};
|
||||||
|
use tokio::net::tcp::WriteHalf;
|
||||||
|
|
||||||
|
use crate::proto::client::{Client, ClientInfo};
|
||||||
|
use crate::proto::packet;
|
||||||
|
|
||||||
|
/// Minecraft channel to set brand.
|
||||||
|
const CHANNEL: &str = "minecraft:brand";
|
||||||
|
|
||||||
|
/// Server brand to send to client in lobby world.
|
||||||
|
///
|
||||||
|
/// Shown in F3 menu. Updated once client is relayed to real server.
|
||||||
|
const SERVER_BRAND: &[u8] = b"lazymc";
|
||||||
|
|
||||||
|
/// Send lobby brand to client.
|
||||||
|
pub async fn send(
|
||||||
|
client: &Client,
|
||||||
|
client_info: &ClientInfo,
|
||||||
|
writer: &mut WriteHalf<'_>,
|
||||||
|
) -> Result<(), ()> {
|
||||||
|
match client_info.protocol() {
|
||||||
|
Some(p) if p < v1_17::PROTOCOL => {
|
||||||
|
packet::write_packet(
|
||||||
|
v1_16_3::game::ClientBoundPluginMessage {
|
||||||
|
channel: CHANNEL.into(),
|
||||||
|
data: SERVER_BRAND.into(),
|
||||||
|
},
|
||||||
|
client,
|
||||||
|
writer,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
packet::write_packet(
|
||||||
|
v1_17::game::ClientBoundPluginMessage {
|
||||||
|
channel: CHANNEL.into(),
|
||||||
|
data: SERVER_BRAND.into(),
|
||||||
|
},
|
||||||
|
client,
|
||||||
|
writer,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
48
src/proto/packets/play/sound.rs
Normal file
48
src/proto/packets/play/sound.rs
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
use minecraft_protocol::version::{v1_16_3, v1_17};
|
||||||
|
use tokio::net::tcp::WriteHalf;
|
||||||
|
|
||||||
|
use crate::proto::client::{Client, ClientInfo};
|
||||||
|
use crate::proto::packet;
|
||||||
|
|
||||||
|
/// Play a sound effect at world origin.
|
||||||
|
pub async fn send(
|
||||||
|
client: &Client,
|
||||||
|
client_info: &ClientInfo,
|
||||||
|
writer: &mut WriteHalf<'_>,
|
||||||
|
sound_name: &str,
|
||||||
|
) -> Result<(), ()> {
|
||||||
|
match client_info.protocol() {
|
||||||
|
Some(p) if p < v1_17::PROTOCOL => {
|
||||||
|
packet::write_packet(
|
||||||
|
v1_16_3::game::NamedSoundEffect {
|
||||||
|
sound_name: sound_name.into(),
|
||||||
|
sound_category: 0,
|
||||||
|
effect_pos_x: 0,
|
||||||
|
effect_pos_y: 0,
|
||||||
|
effect_pos_z: 0,
|
||||||
|
volume: 1.0,
|
||||||
|
pitch: 1.0,
|
||||||
|
},
|
||||||
|
client,
|
||||||
|
writer,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
packet::write_packet(
|
||||||
|
v1_17::game::NamedSoundEffect {
|
||||||
|
sound_name: sound_name.into(),
|
||||||
|
sound_category: 0,
|
||||||
|
effect_pos_x: 0,
|
||||||
|
effect_pos_y: 0,
|
||||||
|
effect_pos_z: 0,
|
||||||
|
volume: 1.0,
|
||||||
|
pitch: 1.0,
|
||||||
|
},
|
||||||
|
client,
|
||||||
|
writer,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
41
src/proto/packets/play/time_update.rs
Normal file
41
src/proto/packets/play/time_update.rs
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
use minecraft_protocol::version::{v1_16_3, v1_17};
|
||||||
|
use tokio::net::tcp::WriteHalf;
|
||||||
|
|
||||||
|
use crate::proto::client::{Client, ClientInfo};
|
||||||
|
use crate::proto::packet;
|
||||||
|
|
||||||
|
/// Send lobby time update to client.
|
||||||
|
///
|
||||||
|
/// Sets world time to 0.
|
||||||
|
///
|
||||||
|
/// Required once for keep-alive packets.
|
||||||
|
pub async fn send(
|
||||||
|
client: &Client,
|
||||||
|
client_info: &ClientInfo,
|
||||||
|
writer: &mut WriteHalf<'_>,
|
||||||
|
) -> Result<(), ()> {
|
||||||
|
match client_info.protocol() {
|
||||||
|
Some(p) if p < v1_17::PROTOCOL => {
|
||||||
|
packet::write_packet(
|
||||||
|
v1_16_3::game::TimeUpdate {
|
||||||
|
world_age: 0,
|
||||||
|
time_of_day: 0,
|
||||||
|
},
|
||||||
|
client,
|
||||||
|
writer,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
packet::write_packet(
|
||||||
|
v1_17::game::TimeUpdate {
|
||||||
|
world_age: 0,
|
||||||
|
time_of_day: 0,
|
||||||
|
},
|
||||||
|
client,
|
||||||
|
writer,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
141
src/proto/packets/play/title.rs
Normal file
141
src/proto/packets/play/title.rs
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
use minecraft_protocol::data::chat::{Message, Payload};
|
||||||
|
use minecraft_protocol::version::{v1_16_3, v1_17};
|
||||||
|
use tokio::net::tcp::WriteHalf;
|
||||||
|
|
||||||
|
#[cfg(feature = "lobby")]
|
||||||
|
use crate::lobby::KEEP_ALIVE_INTERVAL;
|
||||||
|
use crate::mc;
|
||||||
|
use crate::proto::client::{Client, ClientInfo};
|
||||||
|
use crate::proto::packet;
|
||||||
|
|
||||||
|
#[cfg(feature = "lobby")]
|
||||||
|
const DISPLAY_TIME: i32 = KEEP_ALIVE_INTERVAL.as_secs() as i32 * mc::TICKS_PER_SECOND as i32 * 2;
|
||||||
|
#[cfg(not(feature = "lobby"))]
|
||||||
|
const DISPLAY_TIME: i32 = 10 * mc::TICKS_PER_SECOND as i32 * 2;
|
||||||
|
|
||||||
|
/// Send lobby title packets to client.
|
||||||
|
///
|
||||||
|
/// This will show the given text for two keep-alive periods. Use a newline for the subtitle.
|
||||||
|
///
|
||||||
|
/// If an empty string is given, the title times will be reset to default.
|
||||||
|
pub async fn send(
|
||||||
|
client: &Client,
|
||||||
|
client_info: &ClientInfo,
|
||||||
|
writer: &mut WriteHalf<'_>,
|
||||||
|
text: &str,
|
||||||
|
) -> Result<(), ()> {
|
||||||
|
// Grab title and subtitle bits
|
||||||
|
let title = text.lines().next().unwrap_or("");
|
||||||
|
let subtitle = text.lines().skip(1).collect::<Vec<_>>().join("\n");
|
||||||
|
|
||||||
|
match client_info.protocol() {
|
||||||
|
Some(p) if p < v1_17::PROTOCOL => send_v1_16_3(client, writer, title, &subtitle).await,
|
||||||
|
_ => send_v1_17(client, writer, title, &subtitle).await,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn send_v1_16_3(
|
||||||
|
client: &Client,
|
||||||
|
writer: &mut WriteHalf<'_>,
|
||||||
|
title: &str,
|
||||||
|
subtitle: &str,
|
||||||
|
) -> Result<(), ()> {
|
||||||
|
use v1_16_3::game::{Title, TitleAction};
|
||||||
|
|
||||||
|
// Set title
|
||||||
|
packet::write_packet(
|
||||||
|
Title {
|
||||||
|
action: TitleAction::SetTitle {
|
||||||
|
text: Message::new(Payload::text(title)),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
client,
|
||||||
|
writer,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// Set subtitle
|
||||||
|
packet::write_packet(
|
||||||
|
Title {
|
||||||
|
action: TitleAction::SetSubtitle {
|
||||||
|
text: Message::new(Payload::text(subtitle)),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
client,
|
||||||
|
writer,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// Set title times
|
||||||
|
packet::write_packet(
|
||||||
|
Title {
|
||||||
|
action: if title.is_empty() && subtitle.is_empty() {
|
||||||
|
// Defaults: https://minecraft.wiki/w/Commands/title#Detail
|
||||||
|
TitleAction::SetTimesAndDisplay {
|
||||||
|
fade_in: 10,
|
||||||
|
stay: 70,
|
||||||
|
fade_out: 20,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
TitleAction::SetTimesAndDisplay {
|
||||||
|
fade_in: 0,
|
||||||
|
stay: DISPLAY_TIME,
|
||||||
|
fade_out: 0,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
client,
|
||||||
|
writer,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn send_v1_17(
|
||||||
|
client: &Client,
|
||||||
|
writer: &mut WriteHalf<'_>,
|
||||||
|
title: &str,
|
||||||
|
subtitle: &str,
|
||||||
|
) -> Result<(), ()> {
|
||||||
|
use v1_17::game::{SetTitleSubtitle, SetTitleText, SetTitleTimes};
|
||||||
|
|
||||||
|
// Set title
|
||||||
|
packet::write_packet(
|
||||||
|
SetTitleText {
|
||||||
|
text: Message::new(Payload::text(title)),
|
||||||
|
},
|
||||||
|
client,
|
||||||
|
writer,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// Set subtitle
|
||||||
|
packet::write_packet(
|
||||||
|
SetTitleSubtitle {
|
||||||
|
text: Message::new(Payload::text(subtitle)),
|
||||||
|
},
|
||||||
|
client,
|
||||||
|
writer,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// Set title times
|
||||||
|
packet::write_packet(
|
||||||
|
if title.is_empty() && subtitle.is_empty() {
|
||||||
|
// Defaults: https://minecraft.wiki/w/Commands/title#Detail
|
||||||
|
SetTitleTimes {
|
||||||
|
fade_in: 10,
|
||||||
|
stay: 70,
|
||||||
|
fade_out: 20,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
SetTitleTimes {
|
||||||
|
fade_in: 0,
|
||||||
|
stay: DISPLAY_TIME,
|
||||||
|
fade_out: 0,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
client,
|
||||||
|
writer,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
132
src/proxy.rs
132
src/proxy.rs
@@ -1,20 +1,30 @@
|
|||||||
use std::error::Error;
|
use std::error::Error;
|
||||||
use std::net::SocketAddr;
|
use std::net::SocketAddr;
|
||||||
|
|
||||||
|
use bytes::BytesMut;
|
||||||
|
use proxy_protocol::version2::{ProxyAddresses, ProxyCommand, ProxyTransportProtocol};
|
||||||
|
use proxy_protocol::EncodeError;
|
||||||
use tokio::io;
|
use tokio::io;
|
||||||
use tokio::io::AsyncWriteExt;
|
use tokio::io::AsyncWriteExt;
|
||||||
use tokio::net::TcpStream;
|
use tokio::net::TcpStream;
|
||||||
|
|
||||||
|
use crate::net;
|
||||||
|
|
||||||
/// Proxy the inbound stream to a target address.
|
/// Proxy the inbound stream to a target address.
|
||||||
pub async fn proxy(inbound: TcpStream, addr_target: SocketAddr) -> Result<(), Box<dyn Error>> {
|
pub async fn proxy(
|
||||||
proxy_with_queue(inbound, addr_target, &[]).await
|
inbound: TcpStream,
|
||||||
|
proxy_header: ProxyHeader,
|
||||||
|
addr_target: SocketAddr,
|
||||||
|
) -> Result<(), Box<dyn Error>> {
|
||||||
|
proxy_with_queue(inbound, proxy_header, addr_target, &[]).await
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Proxy the inbound stream to a target address.
|
/// Proxy the inbound stream to a target address.
|
||||||
///
|
///
|
||||||
/// Send the queue to the target server before proxying.
|
/// Send the queue to the target server before proxying.
|
||||||
pub async fn proxy_with_queue(
|
pub async fn proxy_with_queue(
|
||||||
mut inbound: TcpStream,
|
inbound: TcpStream,
|
||||||
|
proxy_header: ProxyHeader,
|
||||||
addr_target: SocketAddr,
|
addr_target: SocketAddr,
|
||||||
queue: &[u8],
|
queue: &[u8],
|
||||||
) -> Result<(), Box<dyn Error>> {
|
) -> Result<(), Box<dyn Error>> {
|
||||||
@@ -22,14 +32,48 @@ pub async fn proxy_with_queue(
|
|||||||
// TODO: on connect fail, ping server and redirect to serve_status if offline
|
// TODO: on connect fail, ping server and redirect to serve_status if offline
|
||||||
let mut outbound = TcpStream::connect(addr_target).await?;
|
let mut outbound = TcpStream::connect(addr_target).await?;
|
||||||
|
|
||||||
|
// Add proxy header
|
||||||
|
match proxy_header {
|
||||||
|
ProxyHeader::None => {}
|
||||||
|
ProxyHeader::Local => {
|
||||||
|
let header = local_proxy_header()?;
|
||||||
|
outbound.write_all(&header).await?;
|
||||||
|
}
|
||||||
|
ProxyHeader::Proxy => {
|
||||||
|
let header = stream_proxy_header(&inbound)?;
|
||||||
|
outbound.write_all(&header).await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start proxy on both streams
|
||||||
|
proxy_inbound_outbound_with_queue(inbound, outbound, &[], queue).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Proxy the inbound stream to a target address.
|
||||||
|
///
|
||||||
|
/// Send the queue to the target server before proxying.
|
||||||
|
// TODO: find better name for this
|
||||||
|
pub async fn proxy_inbound_outbound_with_queue(
|
||||||
|
mut inbound: TcpStream,
|
||||||
|
mut outbound: TcpStream,
|
||||||
|
inbound_queue: &[u8],
|
||||||
|
outbound_queue: &[u8],
|
||||||
|
) -> Result<(), Box<dyn Error>> {
|
||||||
let (mut ri, mut wi) = inbound.split();
|
let (mut ri, mut wi) = inbound.split();
|
||||||
let (mut ro, mut wo) = outbound.split();
|
let (mut ro, mut wo) = outbound.split();
|
||||||
|
|
||||||
|
// Forward queued bytes to client once writable
|
||||||
|
if !inbound_queue.is_empty() {
|
||||||
|
wi.writable().await?;
|
||||||
|
trace!(target: "lazymc", "Relaying {} queued bytes to client", inbound_queue.len());
|
||||||
|
wi.write_all(inbound_queue).await?;
|
||||||
|
}
|
||||||
|
|
||||||
// Forward queued bytes to server once writable
|
// Forward queued bytes to server once writable
|
||||||
if !queue.is_empty() {
|
if !outbound_queue.is_empty() {
|
||||||
wo.writable().await?;
|
wo.writable().await?;
|
||||||
trace!(target: "lazymc", "Relaying {} queued bytes to server", queue.len());
|
trace!(target: "lazymc", "Relaying {} queued bytes to server", outbound_queue.len());
|
||||||
wo.write_all(queue).await?;
|
wo.write_all(outbound_queue).await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
let client_to_server = async {
|
let client_to_server = async {
|
||||||
@@ -43,5 +87,81 @@ pub async fn proxy_with_queue(
|
|||||||
|
|
||||||
tokio::try_join!(client_to_server, server_to_client)?;
|
tokio::try_join!(client_to_server, server_to_client)?;
|
||||||
|
|
||||||
|
// Gracefully close connection if not done already
|
||||||
|
net::close_tcp_stream(inbound).await?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Proxy header.
|
||||||
|
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
||||||
|
pub enum ProxyHeader {
|
||||||
|
/// Do not add proxy header.
|
||||||
|
None,
|
||||||
|
|
||||||
|
/// Header for locally initiated connection.
|
||||||
|
#[allow(unused)]
|
||||||
|
Local,
|
||||||
|
|
||||||
|
/// Header for proxied connection.
|
||||||
|
Proxy,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ProxyHeader {
|
||||||
|
/// Changes to `None` if `false` if given.
|
||||||
|
///
|
||||||
|
/// `None` stays `None`.
|
||||||
|
pub fn not_none(self, not_none: bool) -> Self {
|
||||||
|
if not_none {
|
||||||
|
self
|
||||||
|
} else {
|
||||||
|
Self::None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the proxy header for a locally initiated connection.
|
||||||
|
///
|
||||||
|
/// This header may be sent over the outbound stream to signal client information.
|
||||||
|
pub fn local_proxy_header() -> Result<BytesMut, EncodeError> {
|
||||||
|
// Build proxy header
|
||||||
|
let header = proxy_protocol::ProxyHeader::Version2 {
|
||||||
|
command: ProxyCommand::Local,
|
||||||
|
transport_protocol: ProxyTransportProtocol::Stream,
|
||||||
|
addresses: ProxyAddresses::Unspec,
|
||||||
|
};
|
||||||
|
|
||||||
|
proxy_protocol::encode(header)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the proxy header for the given inbound stream.
|
||||||
|
///
|
||||||
|
/// This header may be sent over the outbound stream to signal client information.
|
||||||
|
pub fn stream_proxy_header(inbound: &TcpStream) -> Result<BytesMut, EncodeError> {
|
||||||
|
// Get peer and local address
|
||||||
|
let peer = inbound
|
||||||
|
.peer_addr()
|
||||||
|
.expect("Peer address not known for TCP stream");
|
||||||
|
let local = inbound
|
||||||
|
.local_addr()
|
||||||
|
.expect("Local address not known for TCP stream");
|
||||||
|
|
||||||
|
// Build proxy header
|
||||||
|
let header = proxy_protocol::ProxyHeader::Version2 {
|
||||||
|
command: ProxyCommand::Proxy,
|
||||||
|
transport_protocol: ProxyTransportProtocol::Stream,
|
||||||
|
addresses: match (peer, local) {
|
||||||
|
(SocketAddr::V4(source), SocketAddr::V4(destination)) => ProxyAddresses::Ipv4 {
|
||||||
|
source,
|
||||||
|
destination,
|
||||||
|
},
|
||||||
|
(SocketAddr::V6(source), SocketAddr::V6(destination)) => ProxyAddresses::Ipv6 {
|
||||||
|
source,
|
||||||
|
destination,
|
||||||
|
},
|
||||||
|
(_, _) => unreachable!(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
proxy_protocol::encode(header)
|
||||||
|
}
|
||||||
|
262
src/server.rs
262
src/server.rs
@@ -1,3 +1,4 @@
|
|||||||
|
use std::net::IpAddr;
|
||||||
use std::sync::atomic::{AtomicU8, Ordering};
|
use std::sync::atomic::{AtomicU8, Ordering};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
@@ -11,8 +12,11 @@ use tokio::sync::Semaphore;
|
|||||||
use tokio::sync::{Mutex, RwLock, RwLockReadGuard};
|
use tokio::sync::{Mutex, RwLock, RwLockReadGuard};
|
||||||
use tokio::time;
|
use tokio::time;
|
||||||
|
|
||||||
use crate::config::Config;
|
use crate::config::{Config, Server as ConfigServer};
|
||||||
|
use crate::mc::ban::{BannedIp, BannedIps};
|
||||||
|
use crate::mc::whitelist::Whitelist;
|
||||||
use crate::os;
|
use crate::os;
|
||||||
|
use crate::proto::packets::play::join_game::JoinGameData;
|
||||||
|
|
||||||
/// Server cooldown after the process quit.
|
/// Server cooldown after the process quit.
|
||||||
/// Used to give it some more time to quit forgotten threads, such as for RCON.
|
/// Used to give it some more time to quit forgotten threads, such as for RCON.
|
||||||
@@ -25,44 +29,11 @@ const SERVER_QUIT_COOLDOWN: Duration = Duration::from_millis(2500);
|
|||||||
#[cfg(feature = "rcon")]
|
#[cfg(feature = "rcon")]
|
||||||
const RCON_COOLDOWN: Duration = Duration::from_secs(15);
|
const RCON_COOLDOWN: Duration = Duration::from_secs(15);
|
||||||
|
|
||||||
/// Server state.
|
/// Exit codes that are allowed.
|
||||||
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
|
///
|
||||||
pub enum State {
|
/// - 143: https://github.com/timvisee/lazymc/issues/26#issuecomment-1435670029
|
||||||
/// Server is stopped.
|
/// - 130: https://unix.stackexchange.com/q/386836/61092
|
||||||
Stopped,
|
const ALLOWED_EXIT_CODES: [i32; 2] = [130, 143];
|
||||||
|
|
||||||
/// 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.
|
/// Shared server state.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
@@ -102,6 +73,12 @@ pub struct Server {
|
|||||||
/// Used as starting/stopping timeout.
|
/// Used as starting/stopping timeout.
|
||||||
kill_at: RwLock<Option<Instant>>,
|
kill_at: RwLock<Option<Instant>>,
|
||||||
|
|
||||||
|
/// List of banned IPs.
|
||||||
|
banned_ips: RwLock<BannedIps>,
|
||||||
|
|
||||||
|
/// Whitelist if enabled.
|
||||||
|
whitelist: RwLock<Option<Whitelist>>,
|
||||||
|
|
||||||
/// Lock for exclusive RCON operations.
|
/// Lock for exclusive RCON operations.
|
||||||
#[cfg(feature = "rcon")]
|
#[cfg(feature = "rcon")]
|
||||||
rcon_lock: Semaphore,
|
rcon_lock: Semaphore,
|
||||||
@@ -109,6 +86,14 @@ pub struct Server {
|
|||||||
/// Last time server was stopped over RCON.
|
/// Last time server was stopped over RCON.
|
||||||
#[cfg(feature = "rcon")]
|
#[cfg(feature = "rcon")]
|
||||||
rcon_last_stop: Mutex<Option<Instant>>,
|
rcon_last_stop: Mutex<Option<Instant>>,
|
||||||
|
|
||||||
|
/// Probed join game data.
|
||||||
|
pub probed_join_game: RwLock<Option<JoinGameData>>,
|
||||||
|
|
||||||
|
/// Forge payload.
|
||||||
|
///
|
||||||
|
/// Sent to clients when they connect to lobby. Recorded from server by probe.
|
||||||
|
pub forge_payload: RwLock<Vec<Vec<u8>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Server {
|
impl Server {
|
||||||
@@ -163,11 +148,11 @@ impl Server {
|
|||||||
|
|
||||||
// Update kill at time for starting/stopping state
|
// Update kill at time for starting/stopping state
|
||||||
*self.kill_at.write().await = match new {
|
*self.kill_at.write().await = match new {
|
||||||
State::Starting if config.time.start_timeout > 0 => {
|
State::Starting if config.server.start_timeout > 0 => {
|
||||||
Some(Instant::now() + Duration::from_secs(config.time.start_timeout as u64))
|
Some(Instant::now() + Duration::from_secs(config.server.start_timeout as u64))
|
||||||
}
|
}
|
||||||
State::Stopping if config.time.stop_timeout > 0 => {
|
State::Stopping if config.server.stop_timeout > 0 => {
|
||||||
Some(Instant::now() + Duration::from_secs(config.time.stop_timeout as u64))
|
Some(Instant::now() + Duration::from_secs(config.server.stop_timeout as u64))
|
||||||
}
|
}
|
||||||
_ => None,
|
_ => None,
|
||||||
};
|
};
|
||||||
@@ -234,9 +219,14 @@ impl Server {
|
|||||||
None => info!(target: "lazymc", "Starting server..."),
|
None => info!(target: "lazymc", "Starting server..."),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Unfreeze server if it is frozen
|
||||||
|
#[cfg(unix)]
|
||||||
|
if config.server.freeze_process && unfreeze_server_signal(&config, &server).await {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
// Spawn server in new task
|
// Spawn server in new task
|
||||||
Self::spawn_server_task(config, server);
|
Self::spawn_server_task(config, server);
|
||||||
|
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -252,6 +242,12 @@ impl Server {
|
|||||||
/// This will attempt to stop the server with all available methods.
|
/// This will attempt to stop the server with all available methods.
|
||||||
#[allow(unused_variables)]
|
#[allow(unused_variables)]
|
||||||
pub async fn stop(&self, config: &Config) -> bool {
|
pub async fn stop(&self, config: &Config) -> bool {
|
||||||
|
// Try to freeze through signal
|
||||||
|
#[cfg(unix)]
|
||||||
|
if config.server.freeze_process && freeze_server_signal(config, self).await {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
// Try to stop through RCON if started
|
// Try to stop through RCON if started
|
||||||
#[cfg(feature = "rcon")]
|
#[cfg(feature = "rcon")]
|
||||||
if self.state() == State::Started && stop_server_rcon(config, self).await {
|
if self.state() == State::Started && stop_server_rcon(config, self).await {
|
||||||
@@ -330,7 +326,7 @@ impl Server {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Read last known server status.
|
/// Read last known server status.
|
||||||
pub async fn status<'a>(&'a self) -> RwLockReadGuard<'a, Option<ServerStatus>> {
|
pub async fn status(&self) -> RwLockReadGuard<'_, Option<ServerStatus>> {
|
||||||
self.status.read().await
|
self.status.read().await
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -345,6 +341,59 @@ impl Server {
|
|||||||
.filter(|d| *d > 0)
|
.filter(|d| *d > 0)
|
||||||
.map(|d| Instant::now() + Duration::from_secs(d as u64));
|
.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 })
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check whether the given username is whitelisted.
|
||||||
|
///
|
||||||
|
/// Returns `true` if no whitelist is currently used.
|
||||||
|
pub async fn is_whitelisted(&self, username: &str) -> bool {
|
||||||
|
self.whitelist
|
||||||
|
.read()
|
||||||
|
.await
|
||||||
|
.as_ref()
|
||||||
|
.map(|w| w.is_whitelisted(username))
|
||||||
|
.unwrap_or(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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 })
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Update the whitelist.
|
||||||
|
pub async fn set_whitelist(&self, whitelist: Option<Whitelist>) {
|
||||||
|
*self.whitelist.write().await = whitelist;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Update the whitelist.
|
||||||
|
pub fn set_whitelist_blocking(&self, whitelist: Option<Whitelist>) {
|
||||||
|
futures::executor::block_on(async { self.set_whitelist(whitelist).await })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Server {
|
impl Default for Server {
|
||||||
@@ -360,10 +409,53 @@ impl Default for Server {
|
|||||||
last_active: Default::default(),
|
last_active: Default::default(),
|
||||||
keep_online_until: Default::default(),
|
keep_online_until: Default::default(),
|
||||||
kill_at: Default::default(),
|
kill_at: Default::default(),
|
||||||
|
banned_ips: Default::default(),
|
||||||
|
whitelist: Default::default(),
|
||||||
#[cfg(feature = "rcon")]
|
#[cfg(feature = "rcon")]
|
||||||
rcon_lock: Semaphore::new(1),
|
rcon_lock: Semaphore::new(1),
|
||||||
#[cfg(feature = "rcon")]
|
#[cfg(feature = "rcon")]
|
||||||
rcon_last_stop: Default::default(),
|
rcon_last_stop: Default::default(),
|
||||||
|
probed_join_game: Default::default(),
|
||||||
|
forge_payload: Default::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -380,7 +472,7 @@ pub async fn invoke_server_cmd(
|
|||||||
cmd.kill_on_drop(true);
|
cmd.kill_on_drop(true);
|
||||||
|
|
||||||
// Set working directory
|
// Set working directory
|
||||||
if let Some(ref dir) = config.server.directory {
|
if let Some(ref dir) = ConfigServer::server_directory(&config) {
|
||||||
cmd.current_dir(dir);
|
cmd.current_dir(dir);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -406,6 +498,15 @@ pub async fn invoke_server_cmd(
|
|||||||
debug!(target: "lazymc", "Server process stopped successfully ({})", status);
|
debug!(target: "lazymc", "Server process stopped successfully ({})", status);
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
Ok(status)
|
||||||
|
if status
|
||||||
|
.code()
|
||||||
|
.map(|ref code| ALLOWED_EXIT_CODES.contains(code))
|
||||||
|
.unwrap_or(false) =>
|
||||||
|
{
|
||||||
|
debug!(target: "lazymc", "Server process stopped successfully by SIGTERM ({})", status);
|
||||||
|
false
|
||||||
|
}
|
||||||
Ok(status) => {
|
Ok(status) => {
|
||||||
warn!(target: "lazymc", "Server process stopped with error code ({})", status);
|
warn!(target: "lazymc", "Server process stopped with error code ({})", status);
|
||||||
state.state() == State::Started
|
state.state() == State::Started
|
||||||
@@ -461,13 +562,8 @@ async fn stop_server_rcon(config: &Config, server: &Server) -> bool {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// RCON address
|
|
||||||
let mut addr = config.server.address;
|
|
||||||
addr.set_port(config.rcon.port);
|
|
||||||
let addr = addr.to_string();
|
|
||||||
|
|
||||||
// Create RCON client
|
// Create RCON client
|
||||||
let mut rcon = match Rcon::connect(&addr, &config.rcon.password).await {
|
let mut rcon = match Rcon::connect_config(config).await {
|
||||||
Ok(rcon) => rcon,
|
Ok(rcon) => rcon,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
error!(target: "lazymc", "Failed to RCON server to sleep: {}", err);
|
error!(target: "lazymc", "Failed to RCON server to sleep: {}", err);
|
||||||
@@ -485,11 +581,11 @@ async fn stop_server_rcon(config: &Config, server: &Server) -> bool {
|
|||||||
server.rcon_last_stop.lock().await.replace(Instant::now());
|
server.rcon_last_stop.lock().await.replace(Instant::now());
|
||||||
server.update_state(State::Stopping, config).await;
|
server.update_state(State::Stopping, config).await;
|
||||||
|
|
||||||
drop(rcon_lock);
|
|
||||||
|
|
||||||
// Gracefully close connection
|
// Gracefully close connection
|
||||||
rcon.close().await;
|
rcon.close().await;
|
||||||
|
|
||||||
|
drop(rcon_lock);
|
||||||
|
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -507,13 +603,11 @@ async fn stop_server_signal(config: &Config, server: &Server) -> bool {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Send kill signal
|
|
||||||
if !crate::os::kill_gracefully(pid) {
|
if !crate::os::kill_gracefully(pid) {
|
||||||
error!(target: "lazymc", "Failed to send stop signal to server process");
|
error!(target: "lazymc", "Failed to send stop signal to server process");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update from starting/started to stopping
|
|
||||||
server
|
server
|
||||||
.update_state_from(Some(State::Starting), State::Stopping, config)
|
.update_state_from(Some(State::Starting), State::Stopping, config)
|
||||||
.await;
|
.await;
|
||||||
@@ -523,3 +617,59 @@ async fn stop_server_signal(config: &Config, server: &Server) -> bool {
|
|||||||
|
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Freeze server by sending SIGSTOP signal.
|
||||||
|
///
|
||||||
|
/// Only available on Unix.
|
||||||
|
#[cfg(unix)]
|
||||||
|
async fn freeze_server_signal(config: &Config, server: &Server) -> bool {
|
||||||
|
// Grab PID
|
||||||
|
let pid = match *server.pid.lock().await {
|
||||||
|
Some(pid) => pid,
|
||||||
|
None => {
|
||||||
|
debug!(target: "lazymc", "Could not send freeze signal to server process, PID unknown");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if !os::freeze(pid) {
|
||||||
|
error!(target: "lazymc", "Failed to send freeze signal to server process.");
|
||||||
|
}
|
||||||
|
|
||||||
|
server
|
||||||
|
.update_state_from(Some(State::Starting), State::Stopped, config)
|
||||||
|
.await;
|
||||||
|
server
|
||||||
|
.update_state_from(Some(State::Started), State::Stopped, config)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Unfreeze server by sending SIGCONT signal.
|
||||||
|
///
|
||||||
|
/// Only available on Unix.
|
||||||
|
#[cfg(unix)]
|
||||||
|
async fn unfreeze_server_signal(config: &Config, server: &Server) -> bool {
|
||||||
|
// Grab PID
|
||||||
|
let pid = match *server.pid.lock().await {
|
||||||
|
Some(pid) => pid,
|
||||||
|
None => {
|
||||||
|
debug!(target: "lazymc", "Could not send unfreeze signal to server process, PID unknown");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if !os::unfreeze(pid) {
|
||||||
|
error!(target: "lazymc", "Failed to send unfreeze signal to server process.");
|
||||||
|
}
|
||||||
|
|
||||||
|
server
|
||||||
|
.update_state_from(Some(State::Stopping), State::Starting, config)
|
||||||
|
.await;
|
||||||
|
server
|
||||||
|
.update_state_from(Some(State::Stopped), State::Starting, config)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
true
|
||||||
|
}
|
||||||
|
172
src/service/file_watcher.rs
Normal file
172
src/service/file_watcher.rs
Normal file
@@ -0,0 +1,172 @@
|
|||||||
|
use std::path::Path;
|
||||||
|
use std::sync::mpsc::channel;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use notify::{watcher, DebouncedEvent, RecursiveMode, Watcher};
|
||||||
|
|
||||||
|
use crate::config::{Config, Server as ConfigServer};
|
||||||
|
use crate::mc::ban::{self, BannedIps};
|
||||||
|
use crate::mc::{server_properties, whitelist};
|
||||||
|
use crate::server::Server;
|
||||||
|
|
||||||
|
/// File watcher debounce time.
|
||||||
|
const WATCH_DEBOUNCE: Duration = Duration::from_secs(2);
|
||||||
|
|
||||||
|
/// Service to watch server file changes.
|
||||||
|
pub fn service(config: Arc<Config>, server: Arc<Server>) {
|
||||||
|
// Ensure server directory is set, it must exist
|
||||||
|
let dir = match ConfigServer::server_directory(&config) {
|
||||||
|
Some(dir) if dir.is_dir() => dir,
|
||||||
|
_ => {
|
||||||
|
warn!(target: "lazymc", "Server directory doesn't exist, can't watch file changes to reload whitelist and banned IPs");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Keep watching
|
||||||
|
#[allow(clippy::blocks_in_if_conditions)]
|
||||||
|
while {
|
||||||
|
// Update all files once
|
||||||
|
reload_bans(&config, &server, &dir.join(ban::FILE));
|
||||||
|
reload_whitelist(&config, &server, &dir);
|
||||||
|
|
||||||
|
// Watch for changes, update accordingly
|
||||||
|
watch_server(&config, &server, &dir)
|
||||||
|
} {}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Watch server directory.
|
||||||
|
///
|
||||||
|
/// Returns `true` if we should watch again.
|
||||||
|
#[must_use]
|
||||||
|
fn watch_server(config: &Config, server: &Server, dir: &Path) -> bool {
|
||||||
|
// Directory must exist
|
||||||
|
if !dir.is_dir() {
|
||||||
|
error!(target: "lazymc", "Server directory does not exist at {} anymore, not watching changes", dir.display());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create watcher for directory
|
||||||
|
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(dir, RecursiveMode::NonRecursive) {
|
||||||
|
error!(target: "lazymc", "An error occured while creating watcher for server files: {}", err);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle change events
|
||||||
|
loop {
|
||||||
|
match rx.recv().unwrap() {
|
||||||
|
// Handle file updates
|
||||||
|
DebouncedEvent::Create(ref path)
|
||||||
|
| DebouncedEvent::Write(ref path)
|
||||||
|
| DebouncedEvent::Remove(ref path) => {
|
||||||
|
update(config, server, dir, path);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle file updates on both paths for rename
|
||||||
|
DebouncedEvent::Rename(ref before_path, ref after_path) => {
|
||||||
|
update(config, server, dir, before_path);
|
||||||
|
update(config, server, dir, after_path);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ignore write/remove notices, will receive write/remove event later
|
||||||
|
DebouncedEvent::NoticeWrite(_) | DebouncedEvent::NoticeRemove(_) => {}
|
||||||
|
|
||||||
|
// Ignore chmod changes
|
||||||
|
DebouncedEvent::Chmod(_) => {}
|
||||||
|
|
||||||
|
// Rewatch on rescan
|
||||||
|
DebouncedEvent::Rescan => {
|
||||||
|
debug!(target: "lazymc", "Rescanning server directory files due to file watching problem");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rewatch on error
|
||||||
|
DebouncedEvent::Error(err, _) => {
|
||||||
|
error!(target: "lazymc", "Error occurred while watching server directory for file changes: {}", err);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Process a file change on the given path.
|
||||||
|
///
|
||||||
|
/// Should be called both when created, changed or removed.
|
||||||
|
fn update(config: &Config, server: &Server, dir: &Path, path: &Path) {
|
||||||
|
// Update bans
|
||||||
|
if path.ends_with(ban::FILE) {
|
||||||
|
reload_bans(config, server, path);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update whitelist
|
||||||
|
if path.ends_with(whitelist::WHITELIST_FILE)
|
||||||
|
|| path.ends_with(whitelist::OPS_FILE)
|
||||||
|
|| path.ends_with(server_properties::FILE)
|
||||||
|
{
|
||||||
|
reload_whitelist(config, server, dir);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reload banned IPs.
|
||||||
|
fn reload_bans(config: &Config, server: &Server, path: &Path) {
|
||||||
|
// Bans must be enabled
|
||||||
|
if !config.server.block_banned_ips && !config.server.drop_banned_ips {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
trace!(target: "lazymc", "Reloading banned IPs...");
|
||||||
|
|
||||||
|
// File must exist, clear file otherwise
|
||||||
|
if !path.is_file() {
|
||||||
|
debug!(target: "lazymc", "No banned IPs, {} does not exist", ban::FILE);
|
||||||
|
// warn!(target: "lazymc", "Not blocking banned IPs, {} file does not exist", ban::FILE);
|
||||||
|
server.set_banned_ips_blocking(BannedIps::default());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load and update banned IPs
|
||||||
|
match ban::load(path) {
|
||||||
|
Ok(ips) => server.set_banned_ips_blocking(ips),
|
||||||
|
Err(err) => {
|
||||||
|
debug!(target: "lazymc", "Failed load banned IPs from {}, ignoring: {}", 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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reload whitelisted users.
|
||||||
|
fn reload_whitelist(config: &Config, server: &Server, dir: &Path) {
|
||||||
|
// Whitelist must be enabled
|
||||||
|
if !config.server.wake_whitelist {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Must be enabled in server.properties
|
||||||
|
let enabled = server_properties::read_property(dir.join(server_properties::FILE), "white-list")
|
||||||
|
.map(|v| v.trim() == "true")
|
||||||
|
.unwrap_or(false);
|
||||||
|
if !enabled {
|
||||||
|
server.set_whitelist_blocking(None);
|
||||||
|
debug!(target: "lazymc", "Not using whitelist, not enabled in {}", server_properties::FILE);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
trace!(target: "lazymc", "Reloading whitelisted users...");
|
||||||
|
|
||||||
|
// Load and update whitelisted users
|
||||||
|
match whitelist::load_dir(dir) {
|
||||||
|
Ok(whitelist) => server.set_whitelist_blocking(Some(whitelist)),
|
||||||
|
Err(err) => {
|
||||||
|
debug!(target: "lazymc", "Failed load whitelist from {}, ignoring: {}", dir.display(), err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -1,3 +1,5 @@
|
|||||||
|
pub mod file_watcher;
|
||||||
pub mod monitor;
|
pub mod monitor;
|
||||||
|
pub mod probe;
|
||||||
pub mod server;
|
pub mod server;
|
||||||
pub mod signal;
|
pub mod signal;
|
||||||
|
33
src/service/probe.rs
Normal file
33
src/service/probe.rs
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use crate::config::{Config, Method};
|
||||||
|
use crate::probe;
|
||||||
|
use crate::server::Server;
|
||||||
|
|
||||||
|
/// Probe server.
|
||||||
|
pub async fn service(config: Arc<Config>, state: Arc<Server>) {
|
||||||
|
// Only probe if enabled or if we must
|
||||||
|
if !config.server.probe_on_start && !must_probe(&config) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Probe
|
||||||
|
match probe::probe(config, state).await {
|
||||||
|
Ok(_) => info!(target: "lazymc::probe", "Succesfully probed server"),
|
||||||
|
Err(_) => {
|
||||||
|
error!(target: "lazymc::probe", "Failed to probe server, this may limit lazymc features")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check whether we must probe.
|
||||||
|
fn must_probe(config: &Config) -> bool {
|
||||||
|
// Must probe with lobby and Forge
|
||||||
|
if config.server.forge && config.join.methods.contains(&Method::Lobby) {
|
||||||
|
warn!(target: "lazymc::probe", "Starting server to probe for Forge lobby...");
|
||||||
|
warn!(target: "lazymc::probe", "Set 'server.probe_on_start = true' to remove this warning");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
false
|
||||||
|
}
|
@@ -1,3 +1,4 @@
|
|||||||
|
use std::net::SocketAddr;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use bytes::BytesMut;
|
use bytes::BytesMut;
|
||||||
@@ -5,8 +6,8 @@ use futures::FutureExt;
|
|||||||
use tokio::net::{TcpListener, TcpStream};
|
use tokio::net::{TcpListener, TcpStream};
|
||||||
|
|
||||||
use crate::config::Config;
|
use crate::config::Config;
|
||||||
use crate::proto::Client;
|
use crate::proto::client::Client;
|
||||||
use crate::proxy;
|
use crate::proxy::{self, ProxyHeader};
|
||||||
use crate::server::{self, Server};
|
use crate::server::{self, Server};
|
||||||
use crate::service;
|
use crate::service;
|
||||||
use crate::status;
|
use crate::status;
|
||||||
@@ -38,7 +39,14 @@ pub async fn service(config: Arc<Config>) -> Result<(), ()> {
|
|||||||
config.public.address, config.server.address,
|
config.public.address, config.server.address,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Spawn server monitor and signal handler services
|
if config.lockout.enabled {
|
||||||
|
warn!(
|
||||||
|
target: "lazymc",
|
||||||
|
"Lockout mode is enabled, nobody will be able to connect through the proxy",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Spawn services: monitor, signal handler
|
||||||
tokio::spawn(service::monitor::service(config.clone(), server.clone()));
|
tokio::spawn(service::monitor::service(config.clone(), server.clone()));
|
||||||
tokio::spawn(service::signal::service(config.clone(), server.clone()));
|
tokio::spawn(service::signal::service(config.clone(), server.clone()));
|
||||||
|
|
||||||
@@ -47,6 +55,13 @@ pub async fn service(config: Arc<Config>) -> Result<(), ()> {
|
|||||||
Server::start(config.clone(), server.clone(), None).await;
|
Server::start(config.clone(), server.clone(), None).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Spawn additional services: probe and ban manager
|
||||||
|
tokio::spawn(service::probe::service(config.clone(), server.clone()));
|
||||||
|
tokio::task::spawn_blocking({
|
||||||
|
let (config, server) = (config.clone(), server.clone());
|
||||||
|
|| service::file_watcher::service(config, server)
|
||||||
|
});
|
||||||
|
|
||||||
// Route all incomming connections
|
// Route all incomming connections
|
||||||
while let Ok((inbound, _)) = listener.accept().await {
|
while let Ok((inbound, _)) = listener.accept().await {
|
||||||
route(inbound, config.clone(), server.clone());
|
route(inbound, config.clone(), server.clone());
|
||||||
@@ -58,18 +73,37 @@ pub async fn service(config: Arc<Config>) -> Result<(), ()> {
|
|||||||
/// Route inbound TCP stream to correct service, spawning a new task.
|
/// Route inbound TCP stream to correct service, spawning a new task.
|
||||||
#[inline]
|
#[inline]
|
||||||
fn route(inbound: TcpStream, config: Arc<Config>, server: Arc<Server>) {
|
fn route(inbound: TcpStream, config: Arc<Config>, server: Arc<Server>) {
|
||||||
if server.state() == server::State::Started {
|
// 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 banned && 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)
|
route_proxy(inbound, config)
|
||||||
} else {
|
} else {
|
||||||
route_status(inbound, config, server)
|
route_status(inbound, config, server, peer)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Route inbound TCP stream to status server, spawning a new task.
|
/// Route inbound TCP stream to status server, spawning a new task.
|
||||||
#[inline]
|
#[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
|
// 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| {
|
let service = status::serve(client, inbound, config, server).map(|r| {
|
||||||
if let Err(err) = r {
|
if let Err(err) = r {
|
||||||
warn!(target: "lazymc", "Failed to serve status: {:?}", err);
|
warn!(target: "lazymc", "Failed to serve status: {:?}", err);
|
||||||
@@ -83,7 +117,12 @@ fn route_status(inbound: TcpStream, config: Arc<Config>, server: Arc<Server>) {
|
|||||||
#[inline]
|
#[inline]
|
||||||
fn route_proxy(inbound: TcpStream, config: Arc<Config>) {
|
fn route_proxy(inbound: TcpStream, config: Arc<Config>) {
|
||||||
// When server is online, proxy all
|
// When server is online, proxy all
|
||||||
let service = proxy::proxy(inbound, config.server.address).map(|r| {
|
let service = proxy::proxy(
|
||||||
|
inbound,
|
||||||
|
ProxyHeader::Proxy.not_none(config.server.send_proxy_v2),
|
||||||
|
config.server.address,
|
||||||
|
)
|
||||||
|
.map(|r| {
|
||||||
if let Err(err) = r {
|
if let Err(err) = r {
|
||||||
warn!(target: "lazymc", "Failed to proxy: {}", err);
|
warn!(target: "lazymc", "Failed to proxy: {}", err);
|
||||||
}
|
}
|
||||||
@@ -95,9 +134,25 @@ fn route_proxy(inbound: TcpStream, config: Arc<Config>) {
|
|||||||
/// Route inbound TCP stream to proxy with queued data, spawning a new task.
|
/// Route inbound TCP stream to proxy with queued data, spawning a new task.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn route_proxy_queue(inbound: TcpStream, config: Arc<Config>, queue: BytesMut) {
|
pub fn route_proxy_queue(inbound: TcpStream, config: Arc<Config>, queue: BytesMut) {
|
||||||
|
route_proxy_address_queue(
|
||||||
|
inbound,
|
||||||
|
ProxyHeader::Proxy.not_none(config.server.send_proxy_v2),
|
||||||
|
config.server.address,
|
||||||
|
queue,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Route inbound TCP stream to proxy with given address and queued data, spawning a new task.
|
||||||
|
#[inline]
|
||||||
|
pub fn route_proxy_address_queue(
|
||||||
|
inbound: TcpStream,
|
||||||
|
proxy_header: ProxyHeader,
|
||||||
|
addr: SocketAddr,
|
||||||
|
queue: BytesMut,
|
||||||
|
) {
|
||||||
// When server is online, proxy all
|
// When server is online, proxy all
|
||||||
let service = async move {
|
let service = async move {
|
||||||
proxy::proxy_with_queue(inbound, config.server.address, &queue)
|
proxy::proxy_with_queue(inbound, proxy_header, addr, &queue)
|
||||||
.map(|r| {
|
.map(|r| {
|
||||||
if let Err(err) = r {
|
if let Err(err) = r {
|
||||||
warn!(target: "lazymc", "Failed to proxy: {}", err);
|
warn!(target: "lazymc", "Failed to proxy: {}", err);
|
||||||
|
331
src/status.rs
331
src/status.rs
@@ -1,25 +1,37 @@
|
|||||||
use std::ops::Deref;
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::Duration;
|
|
||||||
|
|
||||||
use crate::server::State;
|
|
||||||
use bytes::BytesMut;
|
use bytes::BytesMut;
|
||||||
use minecraft_protocol::data::chat::{Message, Payload};
|
use minecraft_protocol::data::chat::{Message, Payload};
|
||||||
use minecraft_protocol::data::server_status::*;
|
use minecraft_protocol::data::server_status::*;
|
||||||
use minecraft_protocol::decoder::Decoder;
|
use minecraft_protocol::decoder::Decoder;
|
||||||
use minecraft_protocol::encoder::Encoder;
|
use minecraft_protocol::encoder::Encoder;
|
||||||
use minecraft_protocol::version::v1_14_4::handshake::Handshake;
|
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 minecraft_protocol::version::v1_14_4::status::StatusResponse;
|
||||||
use tokio::io::{self, AsyncWriteExt};
|
use tokio::fs;
|
||||||
use tokio::net::tcp::WriteHalf;
|
use tokio::io::AsyncWriteExt;
|
||||||
use tokio::net::TcpStream;
|
use tokio::net::TcpStream;
|
||||||
use tokio::time;
|
|
||||||
|
|
||||||
use crate::config::*;
|
use crate::config::{Config, Server as ConfigServer};
|
||||||
use crate::proto::{self, Client, ClientState, RawPacket};
|
use crate::join;
|
||||||
|
use crate::mc::favicon;
|
||||||
|
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};
|
use crate::server::{self, Server};
|
||||||
use crate::service;
|
|
||||||
|
/// 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.";
|
||||||
|
|
||||||
|
/// The not-whitelisted kick message.
|
||||||
|
const WHITELIST_MESSAGE: &str = "You are not white-listed on this server!";
|
||||||
|
|
||||||
|
/// Server icon file path.
|
||||||
|
const SERVER_ICON_FILE: &str = "server-icon.png";
|
||||||
|
|
||||||
/// Proxy the given inbound stream to a target address.
|
/// Proxy the given inbound stream to a target address.
|
||||||
// TODO: do not drop error here, return Box<dyn Error>
|
// TODO: do not drop error here, return Box<dyn Error>
|
||||||
@@ -33,11 +45,14 @@ pub async fn serve(
|
|||||||
|
|
||||||
// Incoming buffer and packet holding queue
|
// Incoming buffer and packet holding queue
|
||||||
let mut buf = BytesMut::new();
|
let mut buf = BytesMut::new();
|
||||||
let mut hold_queue = BytesMut::new();
|
|
||||||
|
// Remember inbound packets, track client info
|
||||||
|
let mut inbound_history = BytesMut::new();
|
||||||
|
let mut client_info = ClientInfo::empty();
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
// Read packet from stream
|
// Read packet from stream
|
||||||
let (packet, raw) = match proto::read_packet(&mut buf, &mut reader).await {
|
let (packet, raw) = match packet::read_packet(&client, &mut buf, &mut reader).await {
|
||||||
Ok(Some(packet)) => packet,
|
Ok(Some(packet)) => packet,
|
||||||
Ok(None) => break,
|
Ok(None) => break,
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
@@ -50,199 +65,155 @@ pub async fn serve(
|
|||||||
let client_state = client.state();
|
let client_state = client.state();
|
||||||
|
|
||||||
// Hijack handshake
|
// Hijack handshake
|
||||||
if client_state == ClientState::Handshake && packet.id == proto::STATUS_PACKET_ID_STATUS {
|
if client_state == ClientState::Handshake
|
||||||
// Parse handshake, grab new state
|
&& packet.id == packets::handshake::SERVER_HANDSHAKE
|
||||||
let new_state = match Handshake::decode(&mut packet.data.as_slice()) {
|
{
|
||||||
Ok(handshake) => match ClientState::from_id(handshake.next_state) {
|
// Parse handshake
|
||||||
Some(state) => state,
|
let handshake = match Handshake::decode(&mut packet.data.as_slice()) {
|
||||||
None => {
|
Ok(handshake) => handshake,
|
||||||
error!(target: "lazymc", "Client tried to switch into unknown protcol state ({}), disconnecting", handshake.next_state);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
debug!(target: "lazymc", "Got malformed handshake from client, disconnecting");
|
debug!(target: "lazymc", "Got malformed handshake from client, disconnecting");
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Update client state
|
// Parse new state
|
||||||
|
let new_state = match ClientState::from_id(handshake.next_state) {
|
||||||
|
Some(state) => state,
|
||||||
|
None => {
|
||||||
|
error!(target: "lazymc", "Client tried to switch into unknown protcol state ({}), disconnecting", handshake.next_state);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Update client info and client state
|
||||||
|
client_info
|
||||||
|
.protocol
|
||||||
|
.replace(handshake.protocol_version as u32);
|
||||||
|
client_info.handshake.replace(handshake);
|
||||||
client.set_state(new_state);
|
client.set_state(new_state);
|
||||||
|
|
||||||
// If login handshake and holding is enabled, hold packets
|
// If loggin in with handshake, remember inbound
|
||||||
if new_state == ClientState::Login && config.time.hold() {
|
if new_state == ClientState::Login {
|
||||||
hold_queue.extend(raw);
|
inbound_history.extend(raw);
|
||||||
}
|
}
|
||||||
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hijack server status packet
|
// Hijack server status packet
|
||||||
if client_state == ClientState::Status && packet.id == proto::STATUS_PACKET_ID_STATUS {
|
if client_state == ClientState::Status && packet.id == packets::status::SERVER_STATUS {
|
||||||
let server_status = server_status(&config, &server).await;
|
let server_status = server_status(&client_info, &config, &server).await;
|
||||||
let packet = StatusResponse { server_status };
|
let packet = StatusResponse { server_status };
|
||||||
|
|
||||||
let mut data = Vec::new();
|
let mut data = Vec::new();
|
||||||
packet.encode(&mut data).map_err(|_| ())?;
|
packet.encode(&mut data).map_err(|_| ())?;
|
||||||
|
|
||||||
let response = RawPacket::new(0, data).encode()?;
|
let response = RawPacket::new(0, data).encode_with_len(&client)?;
|
||||||
writer.write_all(&response).await.map_err(|_| ())?;
|
writer.write_all(&response).await.map_err(|_| ())?;
|
||||||
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hijack ping packet
|
// Hijack ping packet
|
||||||
if client_state == ClientState::Status && packet.id == proto::STATUS_PACKET_ID_PING {
|
if client_state == ClientState::Status && packet.id == packets::status::SERVER_PING {
|
||||||
writer.write_all(&raw).await.map_err(|_| ())?;
|
writer.write_all(&raw).await.map_err(|_| ())?;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hijack login start
|
// Hijack login start
|
||||||
if client_state == ClientState::Login && packet.id == proto::LOGIN_PACKET_ID_LOGIN_START {
|
if client_state == ClientState::Login && packet.id == packets::login::SERVER_LOGIN_START {
|
||||||
// Try to get login username
|
// 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())
|
let username = LoginStart::decode(&mut packet.data.as_slice())
|
||||||
.ok()
|
.ok()
|
||||||
.map(|p| p.name);
|
.map(|p| p.name);
|
||||||
|
client_info.username = username.clone();
|
||||||
|
|
||||||
|
// Kick if lockout is enabled
|
||||||
|
if config.lockout.enabled {
|
||||||
|
match username {
|
||||||
|
Some(username) => {
|
||||||
|
info!(target: "lazymc", "Kicked '{}' because lockout is enabled", username)
|
||||||
|
}
|
||||||
|
None => info!(target: "lazymc", "Kicked player because lockout is enabled"),
|
||||||
|
}
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Kick if client is not whitelisted to wake server
|
||||||
|
if let Some(ref username) = username {
|
||||||
|
if !server.is_whitelisted(username).await {
|
||||||
|
info!(target: "lazymc", "User '{}' tried to wake server but is not whitelisted, disconnecting", username);
|
||||||
|
action::kick(&client, WHITELIST_MESSAGE, &mut writer).await?;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Start server if not starting yet
|
// Start server if not starting yet
|
||||||
Server::start(config.clone(), server.clone(), username).await;
|
Server::start(config.clone(), server.clone(), username).await;
|
||||||
|
|
||||||
// Hold client if enabled and starting
|
// Remember inbound packets
|
||||||
if config.time.hold() && server.state() == State::Starting {
|
inbound_history.extend(&raw);
|
||||||
// Hold login packet and remaining read bytes
|
inbound_history.extend(&buf);
|
||||||
hold_queue.extend(raw);
|
|
||||||
hold_queue.extend(buf.split_off(0));
|
|
||||||
|
|
||||||
// Start holding
|
// Build inbound packet queue with everything from login start (including this)
|
||||||
hold(inbound, config, server, hold_queue).await?;
|
let mut login_queue = BytesMut::with_capacity(raw.len() + buf.len());
|
||||||
return Ok(());
|
login_queue.extend(&raw);
|
||||||
}
|
login_queue.extend(&buf);
|
||||||
|
|
||||||
// Select message and kick
|
// Buf is fully consumed here
|
||||||
let msg = match server.state() {
|
buf.clear();
|
||||||
server::State::Starting | server::State::Stopped | server::State::Started => {
|
|
||||||
&config.messages.login_starting
|
|
||||||
}
|
|
||||||
server::State::Stopping => &config.messages.login_stopping,
|
|
||||||
};
|
|
||||||
kick(msg, &mut writer).await?;
|
|
||||||
|
|
||||||
break;
|
// Start occupying client
|
||||||
|
join::occupy(
|
||||||
|
client,
|
||||||
|
client_info,
|
||||||
|
config,
|
||||||
|
server,
|
||||||
|
inbound,
|
||||||
|
inbound_history,
|
||||||
|
login_queue,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show unhandled packet warning
|
// Show unhandled packet warning
|
||||||
debug!(target: "lazymc", "Received unhandled packet:");
|
debug!(target: "lazymc", "Got unhandled packet:");
|
||||||
debug!(target: "lazymc", "- State: {:?}", client_state);
|
debug!(target: "lazymc", "- State: {:?}", client_state);
|
||||||
debug!(target: "lazymc", "- Packet ID: {}", packet.id);
|
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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Hold a client while server starts.
|
|
||||||
///
|
|
||||||
/// Relays client to proxy once server is ready.
|
|
||||||
pub async fn hold<'a>(
|
|
||||||
mut inbound: TcpStream,
|
|
||||||
config: Arc<Config>,
|
|
||||||
server: Arc<Server>,
|
|
||||||
hold_queue: BytesMut,
|
|
||||||
) -> Result<(), ()> {
|
|
||||||
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.time.hold_client_for 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");
|
|
||||||
service::server::route_proxy_queue(inbound, config, hold_queue);
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Server stopping/stopped, this shouldn't happen, kick
|
|
||||||
Ok(false) => {
|
|
||||||
warn!(target: "lazymc", "Server stopping for held client, disconnecting");
|
|
||||||
kick(&config.messages.login_stopping, &mut inbound.split().1).await?;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Timeout reached, kick with starting message
|
|
||||||
Err(_) => {
|
|
||||||
warn!(target: "lazymc", "Held client reached timeout of {}s, disconnecting", config.time.hold_client_for);
|
|
||||||
kick(&config.messages.login_starting, &mut inbound.split().1).await?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Gracefully close connection
|
|
||||||
match inbound.shutdown().await {
|
|
||||||
Ok(_) => {}
|
|
||||||
Err(err) if err.kind() == io::ErrorKind::NotConnected => {}
|
|
||||||
Err(_) => return Err(()),
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Kick client with a message.
|
|
||||||
///
|
|
||||||
/// Should close connection afterwards.
|
|
||||||
async fn kick(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(0, data).encode()?;
|
|
||||||
writer.write_all(&response).await.map_err(|_| ())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Build server status object to respond to client with.
|
/// Build server status object to respond to client with.
|
||||||
async fn server_status(config: &Config, server: &Server) -> ServerStatus {
|
async fn server_status(client_info: &ClientInfo, config: &Config, server: &Server) -> ServerStatus {
|
||||||
let status = server.status().await;
|
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
|
// Select version and player max from last known server status
|
||||||
let (version, max) = match status.as_ref() {
|
let (version, max) = match status.as_ref() {
|
||||||
@@ -258,17 +229,28 @@ async fn server_status(config: &Config, server: &Server) -> ServerStatus {
|
|||||||
|
|
||||||
// Select description, use server MOTD if enabled, or use configured
|
// Select description, use server MOTD if enabled, or use configured
|
||||||
let description = {
|
let description = {
|
||||||
if config.messages.use_server_motd && status.is_some() {
|
if config.motd.from_server && status.is_some() {
|
||||||
status.as_ref().unwrap().description.clone()
|
status.as_ref().unwrap().description.clone()
|
||||||
} else {
|
} else {
|
||||||
Message::new(Payload::text(match server.state() {
|
match server_state {
|
||||||
server::State::Stopped | server::State::Started => &config.messages.motd_sleeping,
|
server::State::Stopped | server::State::Started => &config.motd.sleeping,
|
||||||
server::State::Starting => &config.messages.motd_starting,
|
server::State::Starting => &config.motd.starting,
|
||||||
server::State::Stopping => &config.messages.motd_stopping,
|
server::State::Stopping => &config.motd.stopping,
|
||||||
}))
|
}.to_string()
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Extract favicon from real server status, load from disk, or use default
|
||||||
|
let mut favicon = None;
|
||||||
|
if favicon::supports_favicon(client_info) {
|
||||||
|
if config.motd.from_server && status.is_some() {
|
||||||
|
favicon = status.as_ref().unwrap().favicon.clone()
|
||||||
|
}
|
||||||
|
if favicon.is_none() {
|
||||||
|
favicon = Some(server_favicon(config).await);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Build status resposne
|
// Build status resposne
|
||||||
ServerStatus {
|
ServerStatus {
|
||||||
version,
|
version,
|
||||||
@@ -278,5 +260,36 @@ async fn server_status(config: &Config, server: &Server) -> ServerStatus {
|
|||||||
max,
|
max,
|
||||||
sample: vec![],
|
sample: vec![],
|
||||||
},
|
},
|
||||||
|
favicon,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get server status favicon.
|
||||||
|
///
|
||||||
|
/// This always returns a favicon, returning the default one if none is set.
|
||||||
|
async fn server_favicon(config: &Config) -> String {
|
||||||
|
// Get server dir
|
||||||
|
let dir = match ConfigServer::server_directory(config) {
|
||||||
|
Some(dir) => dir,
|
||||||
|
None => return favicon::default_favicon(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Server icon file, ensure it exists
|
||||||
|
let path = dir.join(SERVER_ICON_FILE);
|
||||||
|
if !path.is_file() {
|
||||||
|
return favicon::default_favicon();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read icon data
|
||||||
|
let data = match fs::read(path).await.map_err(|err| {
|
||||||
|
error!(target: "lazymc", "Failed to read favicon from {}: {}", SERVER_ICON_FILE, err);
|
||||||
|
}) {
|
||||||
|
Ok(data) => data,
|
||||||
|
Err(err) => {
|
||||||
|
error!(target: "lazymc::status", "Failed to load server icon from disk, using default: {:?}", err);
|
||||||
|
return favicon::default_favicon();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
favicon::encode_favicon(&data)
|
||||||
|
}
|
||||||
|
@@ -10,7 +10,7 @@ use crate::util::error::{quit_error, ErrorHints};
|
|||||||
/// excluding the `:` suffix.
|
/// excluding the `:` suffix.
|
||||||
pub fn prompt(msg: &str) -> String {
|
pub fn prompt(msg: &str) -> String {
|
||||||
// Show the prompt
|
// Show the prompt
|
||||||
eprint!("{}: ", msg);
|
eprint!("{msg}: ");
|
||||||
let _ = stderr().flush();
|
let _ = stderr().flush();
|
||||||
|
|
||||||
// Get the input
|
// Get the input
|
||||||
@@ -49,7 +49,7 @@ pub fn prompt_yes(msg: &str, def: Option<bool>) -> bool {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Get the user input
|
// Get the user input
|
||||||
let answer = prompt(&format!("{} {}", msg, options));
|
let answer = prompt(&format!("{msg} {options}"));
|
||||||
|
|
||||||
// Assume the default if the answer is empty
|
// Assume the default if the answer is empty
|
||||||
if answer.is_empty() {
|
if answer.is_empty() {
|
||||||
|
@@ -16,7 +16,7 @@ pub fn print_error(err: anyhow::Error) {
|
|||||||
// Report each printable error, count them
|
// Report each printable error, count them
|
||||||
let count = err
|
let count = err
|
||||||
.chain()
|
.chain()
|
||||||
.map(|err| format!("{}", err))
|
.map(|err| err.to_string())
|
||||||
.filter(|err| !err.is_empty())
|
.filter(|err| !err.is_empty())
|
||||||
.enumerate()
|
.enumerate()
|
||||||
.map(|(i, err)| {
|
.map(|(i, err)| {
|
||||||
@@ -126,7 +126,7 @@ impl ErrorHints {
|
|||||||
if self.config_generate {
|
if self.config_generate {
|
||||||
eprintln!(
|
eprintln!(
|
||||||
"Use '{}' to generate a new config file",
|
"Use '{}' to generate a new config file",
|
||||||
highlight(&format!("{} config generate", bin))
|
highlight(&format!("{bin} config generate"))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if self.config {
|
if self.config {
|
||||||
@@ -138,7 +138,7 @@ impl ErrorHints {
|
|||||||
if self.config_test {
|
if self.config_test {
|
||||||
eprintln!(
|
eprintln!(
|
||||||
"Use '{}' to test a config file",
|
"Use '{}' to test a config file",
|
||||||
highlight(&format!("{} config test -c FILE", bin))
|
highlight(&format!("{bin} config test -c FILE"))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if self.verbose {
|
if self.verbose {
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
pub mod cli;
|
pub mod cli;
|
||||||
pub mod error;
|
pub mod error;
|
||||||
|
pub mod serde;
|
||||||
pub mod style;
|
pub mod style;
|
||||||
|
|
||||||
use std::env;
|
use std::env;
|
||||||
|
30
src/util/serde.rs
Normal file
30
src/util/serde.rs
Normal 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")
|
||||||
|
})
|
||||||
|
}
|
Reference in New Issue
Block a user