Compare commits

...

109 Commits

Author SHA1 Message Date
Tim Visée
37fdb9c12a
Merge pull request #54 from Spongecade/patch-1
Update Minecraft wiki links to new domain
2023-10-13 18:53:23 +02:00
Spongecade
e7a3db19aa
Update Minecraft wiki links to new domain 2023-10-05 15:26:24 -05:00
timvisee
ca4753673d
Run CI on shared GitLab runners 2023-08-20 17:33:32 +02:00
timvisee
0124aa723d
Merge branch 'Xeyler-master' into master
Refs: https://github.com/timvisee/lazymc/pull/50
2023-05-30 22:41:04 +02:00
Xeyler
afbc54758c
Update monitor.rs 2023-05-27 23:14:48 -06:00
timvisee
e54025f02f
Bump version to 0.2.10 2023-02-20 10:34:06 +01:00
timvisee
023e46fe64
Allow exit code 143 and 130
Fixes https://github.com/timvisee/lazymc/issues/26
2023-02-20 10:31:04 +01:00
timvisee
6622962d5d
Bump version to 0.2.9 2023-02-14 13:25:41 +01:00
timvisee
da60287e10
Update dependencies 2023-02-14 13:21:12 +01:00
timvisee
eb5ee7defd
Merge branch 'tW4r-patch-1' into master
See https://github.com/timvisee/lazymc/pull/44
2023-02-14 13:17:55 +01:00
tW4r
fdeb7594c2
Mixing up my booleans myself 2023-02-14 13:49:01 +02:00
tW4r
5c7e17b0ae
Only drop banned IPs when drop_banned_ip
Attempted fix of #43
2023-02-14 13:47:01 +02:00
timvisee
be74e053f4
Resolve clippy warnings 2023-02-10 11:11:05 +01:00
timvisee
eb2cf1219e
Set MSRV in clippy configuration 2023-02-10 11:08:41 +01:00
timvisee
72d58f051f
Bump version to 0.2.8 2023-01-30 19:07:40 +01:00
timvisee
6c3129a8b0
Enable all CI build jobs for tag releases, don't allow Windows failures 2023-01-30 19:07:05 +01:00
timvisee
cc2061ad7d
Fix build failure on Windows 2023-01-30 18:40:43 +01:00
timvisee
342a55471c
Bump Rust MSRV to 1.64.0 2023-01-30 18:38:57 +01:00
timvisee
982a604d34
Install proper Rust version on Windows CI jobs 2023-01-30 18:37:08 +01:00
timvisee
6b463ac8c8
Update dependencies 2023-01-30 18:23:58 +01:00
Tim Visée
c7caebe6a8 Merge branch 'github-pr-37' into 'master'
Implement server process freezing on Unix platforms

See merge request timvisee/lazymc!3
2023-01-30 17:22:12 +00:00
timvisee
aa1a74682e
Fix incorrect state after unfreezing process 2023-01-30 18:18:28 +01:00
timvisee
835ca62c06
Change default protocol version to 1.19.3 (761) 2023-01-30 18:08:23 +01:00
timvisee
b609f86bde
Require to build with rcon feature on Windows 2023-01-30 18:06:28 +01:00
timvisee
4b1857f48d
Don't disable process freeze in config on Windows, simply ignore setting 2023-01-30 17:57:49 +01:00
timvisee
bc7bd908f6
Fix process freezing not working when rcon is enabled 2023-01-30 17:54:20 +01:00
timvisee
a3fef88eac
Don't use deprecated base64 method 2023-01-30 17:42:02 +01:00
[object Object]
57117b29f3
Merge remote-tracking branch 'upstream/master' 2023-01-25 14:46:14 -08:00
[object Object]
5ef7c54ec6
Implement suggested changes 2023-01-25 14:45:15 -08:00
timvisee
5f13132c57
Run release jobs in CI for release tag only 2023-01-25 20:47:43 +01:00
timvisee
226215479c
Run many jobs on master branch only 2023-01-25 20:45:55 +01:00
timvisee
f4870c66fa
Fix scoop installation on Windows in GitLab CI 2023-01-25 20:45:45 +01:00
timvisee
c415420eae
Build Linux jobs CI jobs on custom runner by default 2023-01-25 20:45:21 +01:00
timvisee
c6db4d7c3f
Use Rust stable and MSRV in GitLab CI 2023-01-25 20:45:17 +01:00
timvisee
39feb0bdc2
Remove and disable some Windows CI jobs 2023-01-25 20:35:27 +01:00
timvisee
35fff7168b
Disable Rust build cache in CI 2023-01-25 20:33:25 +01:00
timvisee
e5e5947a16
Remove macOS builds from GitLab CI, instruct to compile in README 2023-01-25 20:31:22 +01:00
[object Object]
2e6551b009
Merge remote-tracking branch 'upstream/master' 2023-01-22 16:49:32 -08:00
timvisee
2c00dba5e8
Update dependencies 2023-01-11 12:46:34 +01:00
timvisee
0e4d18e9f6
Update clap dependency 2023-01-11 12:44:00 +01:00
timvisee
026aa58b5d
Fix broken documentation link
Mentioned in https://github.com/timvisee/lazymc/issues/39
2023-01-10 21:33:09 +01:00
[object Object]
0ac9a07c93
add a todo here because it isn't an ideal solution 2022-12-30 13:32:39 -08:00
[object Object]
540137b93e
disable freeze_process on windows 2022-12-30 13:32:11 -08:00
[object Object]
05dfd19d80
Implement config handling 2022-12-30 13:17:04 -08:00
[object Object]
f02217abd3
Update version number (should be 1 above current release)
Also update deps
2022-12-30 13:10:14 -08:00
[object Object]
3185ca855c
Add config option for process freezing 2022-12-30 13:09:07 -08:00
[object Object]
2c43446ed0
Error handling and logging 2022-12-28 14:37:08 -08:00
[object Object]
f7fe00aa50
Add nix crate, use it for signaling 2022-12-28 14:24:21 -08:00
[object Object]
f2087792b4
Update deps 2022-12-28 14:09:08 -08:00
[object Object]
d46f8375c7
Fix server infinitly stopping 2022-12-28 12:40:09 -08:00
[object Object]
2d8173aba8
Optimize release builds (and format toml file) 2022-12-28 12:33:05 -08:00
[object Object]
bd9f81f1f0
It would be a good idea to actually start the server 2022-12-28 12:29:17 -08:00
[object Object]
b561351a2a
Scuffed code, not tested yet 2022-12-28 12:18:43 -08:00
[object Object]
d3cb880dd0
cargo clippy is angry 2022-12-28 12:03:00 -08:00
[object Object]
716cd48eac
Add SIGSTOP and SIGCONT functions, fix some docs stuff 2022-12-28 12:00:32 -08:00
timvisee
c6f860f013
Update dependencies 2022-06-19 02:12:20 +02:00
dependabot[bot]
8d4ace60a6
Bump regex from 1.5.4 to 1.5.6
Bumps [regex](https://github.com/rust-lang/regex) from 1.5.4 to 1.5.6.
- [Release notes](https://github.com/rust-lang/regex/releases)
- [Changelog](https://github.com/rust-lang/regex/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/regex/compare/1.5.4...1.5.6)

---
updated-dependencies:
- dependency-name: regex
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-06-06 22:00:45 +00:00
timvisee
09c6d1d996
Allow Windows CI to fail because it causes a lot of issues 2022-03-09 14:15:23 +01:00
timvisee
dca2f8eb5c
Install missing Rust target on Windows CI 2022-02-07 17:33:20 +01:00
timvisee
1f6665a90f
Update dependencies 2022-02-07 17:19:59 +01:00
timvisee
0c77a18d96
Install GCC on Windows CI 2022-02-07 16:52:03 +01:00
timvisee
0c69752fec
Bump version to 0.2.7 2021-12-13 17:03:34 +01:00
timvisee
6e8ff5b1b3
Update dependencies 2021-12-13 17:03:27 +01:00
timvisee
fba581d4bd
Update default Minecraft version to 1.18.1 2021-12-13 17:00:43 +01:00
timvisee
d7b601d6e3
Bump version to 0.2.6 2021-11-28 22:20:20 +01:00
timvisee
4d76058472
Update dependencies 2021-11-28 22:18:39 +01:00
timvisee
5b5a2bf8ae
Only use wake whitelist if enabled in server.properties 2021-11-28 22:00:59 +01:00
timvisee
c477e45553
Add server whitelist support, use generic server files watcher to reload 2021-11-28 21:45:52 +01:00
timvisee
69de7a95bf
Bump version to 0.2.5 2021-11-25 15:04:39 +01:00
timvisee
3e7e5c4c03
Fix Windows CI builds having incorrect feature configuration 2021-11-25 15:04:21 +01:00
timvisee
4d774cd254
Update dependencies 2021-11-25 14:54:05 +01:00
timvisee
a1324d22a7
Various fixes and improvements 2021-11-25 14:41:43 +01:00
timvisee
a500f50064
Use probed server settings in lobby join game packet 2021-11-25 14:36:50 +01:00
timvisee
4ef1481f2b
Fix unsupported dimension data in lobby, use proper default world name 2021-11-25 14:21:29 +01:00
timvisee
addfb1c135
Don't use lobby join method if server isn't probed yet when required 2021-11-25 14:20:58 +01:00
timvisee
956c428251
Add compile time feature compatibility check, remove from build.rs 2021-11-25 14:00:59 +01:00
timvisee
270362b152
Use consistent documentation file names 2021-11-25 13:51:24 +01:00
timvisee
93f75adc5c
Update lobby limitations, describe whitelist issue with probe user 2021-11-25 13:50:15 +01:00
timvisee
40fe5b5dd2
Cleanup documentation 2021-11-25 13:42:57 +01:00
timvisee
e7d2c6f64c
Resolve clippy warnings 2021-11-25 13:39:52 +01:00
timvisee
084c3c5f8b
Fix compilation error 2021-11-25 13:27:44 +01:00
timvisee
d39ad9c913
Update TODO 2021-11-25 12:59:52 +01:00
timvisee
00b6cddd78
Change ban list reload log message from info to debug 2021-11-25 12:59:51 +01:00
timvisee
b8744aaf57
Fix protocol versions being handled incorrectly 2021-11-25 12:59:51 +01:00
timvisee
51d3ecf148
Report online mode error if lobby receives server encryption request 2021-11-25 12:59:51 +01:00
timvisee
75c7a09249
Handle unrecognized login plugin requests messages in lobby 2021-11-25 12:59:50 +01:00
timvisee
f6d60318e8
Support Minecraft v1.16.3 to v1.17.1 in lobby and probe logic 2021-11-25 12:59:50 +01:00
timvisee
b404ab0a87
Support multiple Minecraft protocol versions in lobby and probe logic 2021-11-25 12:59:50 +01:00
timvisee
cf6bd526d9
Dynamically build lobby dimension from probed server dimension codec 2021-11-25 12:59:48 +01:00
timvisee
f513957bff
Add config option to enable server probing on start 2021-11-25 12:59:32 +01:00
timvisee
9b1f2a7011
Probe server on lazymc start to get up-to-date server details for Forge 2021-11-25 12:59:32 +01:00
timvisee
8b09faae3d
Constrain supported lobby versions 2021-11-25 12:59:32 +01:00
timvisee
e23a61ab0f
Replay client handshake when lobby connects to real server 2021-11-25 12:59:31 +01:00
timvisee
78a36978f5
Fix broken italic text in Windows usage guide 2021-11-24 14:09:03 +01:00
timvisee
0ba46caf5c
Bump version to 0.2.4 2021-11-24 14:05:27 +01:00
timvisee
6b23490919
Resolve clippy warnings 2021-11-24 14:02:08 +01:00
timvisee
9e08ed6cda
Update dependencies 2021-11-24 13:57:19 +01:00
timvisee
3271db1cb3
Resolve compile warning 2021-11-24 13:55:23 +01:00
timvisee
cf0e3ef15b
Make server directory relative to configuration file directory 2021-11-24 13:53:38 +01:00
timvisee
ee21eb45fd
Bump minimum supported Minecraft version to 1.7.2 2021-11-24 13:28:06 +01:00
timvisee
aebb5563e0
Only send status response favicon to client versions that support it 2021-11-24 13:27:42 +01:00
timvisee
20fb6ee715
Always include favicon in status response, fall back to default icon
See https://github.com/timvisee/lazymc/issues/11#issuecomment-977814539
2021-11-24 13:20:49 +01:00
timvisee
ea2dbc905c
Add Minecraft default server icon, extracted from Minecraft 1.17.1 2021-11-24 13:02:41 +01:00
timvisee
eb66265670
Handle SIGTERM exit code as successful 2021-11-23 13:00:10 +01:00
timvisee
df101ce53b
Fix typo in Cargo.toml key 2021-11-22 20:38:30 +01:00
timvisee
8f2ce9b4b8
Fix compilation error without lobby feature 2021-11-22 20:36:46 +01:00
timvisee
20902e6a94
Derive correct UUID for offline players in lobby logic (2/2) 2021-11-22 20:20:08 +01:00
timvisee
3e933f7566
Derive correct UUID for offline players in lobby logic 2021-11-22 20:14:29 +01:00
timvisee
46fa594065
Update features in README 2021-11-22 19:00:36 +01:00
60 changed files with 3245 additions and 1120 deletions

View File

@ -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,54 +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 --verbose
- cargo check --no-default-features --features rcon --verbose - cargo check --no-default-features --features rcon --verbose
- cargo check --no-default-features --features lobby --verbose - cargo check --no-default-features --features lobby --verbose
check: 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
- cargo check --locked --no-default-features --features lobby --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
@ -90,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
@ -113,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
@ -148,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
@ -179,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
@ -225,29 +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 --verbose
- cargo test --locked --no-default-features --features rcon --verbose - cargo test --locked --no-default-features --features rcon --verbose
- cargo test --locked --no-default-features --features lobby --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 --verbose # - cargo test --locked --no-default-features --features rcon,lobby --verbose
- cargo test --locked --no-default-features --features lobby --verbose
# Release binaries on GitLab as generic package # Release binaries on GitLab as generic package
release-gitlab-generic-package: release-gitlab-generic-package:
@ -258,16 +221,14 @@ release-gitlab-generic-package:
- 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
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:
@ -284,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}
@ -294,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:
@ -315,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:
@ -347,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

View File

@ -1,5 +1,53 @@
# 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) ## 0.2.3 (2021-11-22)
- Add support for `PROXY` header to notify Minecraft server of real client IP - Add support for `PROXY` header to notify Minecraft server of real client IP

952
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
[package] [package]
name = "lazymc" name = "lazymc"
version = "0.2.3" 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,16 +8,15 @@ 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", "lobby"] default = ["rcon", "lobby"]
@ -28,44 +27,71 @@ rcon = ["rust_rcon", "async-std"]
# Lobby support # Lobby support
# Add lobby join method, keeps client in fake lobby world until server is ready. # Add lobby join method, keeps client in fake lobby world until server is ready.
lobby = ["named-binary-tag", "quartz_nbt", "uuid"] lobby = ["md-5", "uuid"]
[dependencies] [dependencies]
anyhow = "1.0" anyhow = "1.0"
base64 = "0.13" base64 = "0.21"
bytes = "1.1" bytes = "1.1"
chrono = "0.4" chrono = "0.4"
clap = { version = "3.0.0-beta.5", default-features = false, features = [ "std", "cargo", "color", "env", "suggestions", "unicode" ]} 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"
flate2 = { version = "1.0", default-features = false, features = ["default"] } flate2 = { version = "1.0", default-features = false, features = ["default"] }
futures = { version = "0.3", default-features = false, features = ["executor"] } 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 = "356ea54" } minecraft-protocol = { git = "https://github.com/timvisee/rust-minecraft-protocol", rev = "edfdf87" }
named-binary-tag = "0.6"
nix = "0.26"
notify = "4.0" notify = "4.0"
pretty_env_logger = "0.4" pretty_env_logger = "0.4"
proxy-protocol = "0.5" proxy-protocol = "0.5"
quartz_nbt = "0.2"
rand = "0.8" rand = "0.8"
serde = "1.0" serde = "1.0"
serde_json = "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", "fs"] } 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" version-compare = "0.1"
# Feature: rcon # Feature: rcon
rust_rcon = { package = "rcon", version = "0.5.2", optional = true } rust_rcon = { package = "rcon", version = "0.5.2", optional = true }
async-std = { version = "1.9.0", deafult-features = false, optional = true } async-std = { version = "1.9.0", default-features = false, optional = true }
# Feature: lobby # Feature: lobby
named-binary-tag = { version = "0.6", optional = true } md-5 = { version = "0.10", optional = true }
quartz_nbt = { version = "0.2", optional = true }
uuid = { version = "0.7", optional = true, features = ["v3"] } 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",
] }

View File

@ -35,7 +35,7 @@ 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)
- Configure joining client occupation methods: - Configure joining client occupation methods:
- Hold: hold clients when server starts, relay when ready, without them noticing - Hold: hold clients when server starts, relay when ready, without them noticing
- Kick: kick clients when server starts, with a starting message - Kick: kick clients when server starts, with a starting message
@ -43,8 +43,9 @@ https://user-images.githubusercontent.com/856222/141378688-882082be-9efa-4cfe-81
- _Lobby: keep client in emulated server with lobby world, teleport to real server when ready ([experimental*](./docs/join-method-lobby.md))_ - _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)
- Automatically block banned IPs from server within `lazymc` - Automatically block banned IPs from server within lazymc
- Graceful server sleep/shutdown through RCON (with `SIGTERM` fallback on Linux/Unix) - 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 - Lockout mode
@ -54,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._
@ -66,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:
@ -76,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)
@ -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)_

View File

@ -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
View File

@ -0,0 +1 @@
msrv = "1.64.0"

41
docs/command-bash.md Normal file
View 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
```

View File

@ -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
View File

@ -0,0 +1 @@
command-bash.md

View File

@ -3,7 +3,7 @@
Some extra steps and recommendations when using lazymc: Some extra steps and recommendations when using lazymc:
Before you use this in production, always ensure starting and stopping the 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 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 it starts and stops. If stopping results in errors, fix this first to prevent
corrupting world/user data. corrupting world/user data.

View File

@ -7,7 +7,7 @@ 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 server is starting. When the server is ready, the player is _teleported_ to the
real server. real server.
`lazymc` emulates a fake server with an empty lobby world. The player is put in 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 this world, floating in space. A custom message is shown on the client to notify
we're waiting on the server to start. we're waiting on the server to start.
@ -21,9 +21,11 @@ enable this in a production environment.
Current limitations: Current limitations:
- Only works with offline mode - Server must be in offline mode (`online-mode=false`)
- Only works with vanilla Minecraft clients, does not work with modded (e.g. Forge, FTB) - Server must use Minecraft version 1.16.3 to 1.17.1 (tested with 1.17.1)
- Probably only works with Minecraft 1.16-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. - 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, At this time it is unknown if some of the above limitations will ever be lifted,
@ -60,15 +62,18 @@ Then configure the lobby to your likings:
# The client will be teleported to the real server once it is ready. # 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. # This may keep the client occupied forever if no timeout is set.
# Consumes client, not allowing other join methods afterwards. # Consumes client, not allowing other join methods afterwards.
# See: https://git.io/JMIi4
# !!! WARNING !!! # !!! WARNING !!!
# This is highly experimental and unstable. # This is highly experimental, incomplete and unstable.
# This may break the game and crash clients. # This may break the game and crash clients.
# Don't enable this unless you know what you're doing. # Don't enable this unless you know what you're doing.
# #
# - Only works with offline mode # - Server must be in offline mode
# - Only works with vanilla Minecraft clients, does not work with modded # - Server must use Minecraft version 1.16.3 to 1.17.1 (tested with 1.17.1)
# - Only tested with Minecraft 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. # Maximum time in seconds in the lobby while the server starts.
timeout = 600 timeout = 600
@ -85,3 +90,25 @@ ready_sound = "block.note_block.chime"
_Note: this might have changed, see the latest configuration _Note: this might have changed, see the latest configuration
[here](../res/lazymc.toml)._ [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.

View File

@ -10,7 +10,7 @@ gets a new protocol version.
## Configuration ## Configuration
In `lazymc` you may configure what protocol version to use: In lazymc you may configure what protocol version to use:
[`lazymc.toml`](../res/lazymc.toml): [`lazymc.toml`](../res/lazymc.toml):
@ -21,8 +21,8 @@ In `lazymc` you may configure what protocol version to use:
# 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://git.io/J1Fvx # See: https://git.io/J1Fvx
version = "1.17.1" version = "1.19.3"
protocol = 756 protocol = 761
# -- snip -- # -- snip --
``` ```
@ -35,5 +35,5 @@ allow the best compatibility with clients.
- Set `public.version` to any string you like. Shows up in read in clients that - Set `public.version` to any string you like. Shows up in read in clients that
have an incompatibel protocol version number have an incompatibel protocol version number
These are used as hint. `lazymc` will automatically use the protocol version of These are used as hint. lazymc will automatically use the protocol version of
your Minecraft server once it has started at least once. your Minecraft server once it has started at least once.

View File

@ -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:
@ -39,6 +39,6 @@ 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

View File

@ -18,8 +18,8 @@
# 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://git.io/J1Fvx # 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,19 +30,33 @@
directory = "." directory = "."
# Command to start the server. # Command to start the server.
# Warning: if using a bash script read: https://git.io/J1FvZ # 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. # Server start/stop timeout in seconds. Force kill server process if it takes too long.
#start_timeout = 300 #start_timeout = 300
#stop_timeout = 150 #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 as listed in banned-ips.json in server directory.
#block_banned_ips = true #block_banned_ips = true
@ -120,15 +134,18 @@ command = "java -Xmx1G -Xms1G -jar server.jar --nogui"
# The client will be teleported to the real server once it is ready. # 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. # This may keep the client occupied forever if no timeout is set.
# Consumes client, not allowing other join methods afterwards. # Consumes client, not allowing other join methods afterwards.
# See: https://git.io/JMIi4
# !!! WARNING !!! # !!! WARNING !!!
# This is highly experimental, incomplete and unstable. # This is highly experimental, incomplete and unstable.
# This may break the game and crash clients. # This may break the game and crash clients.
# Don't enable this unless you know what you're doing. # Don't enable this unless you know what you're doing.
# #
# - Only works with offline mode # - Server must be in offline mode
# - Only works with vanilla Minecraft clients, does not work with modded # - Server must use Minecraft version 1.16.3 to 1.17.1 (tested with 1.17.1)
# - Only tested with Minecraft 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. # Maximum time in seconds in the lobby while the server starts.
#timeout = 600 #timeout = 600
@ -170,4 +187,4 @@ command = "java -Xmx1G -Xms1G -jar server.jar --nogui"
[config] [config]
# lazymc version this configuration is for. # lazymc version this configuration is for.
# Don't change unless you know what you're doing. # Don't change unless you know what you're doing.
version = "0.2.3" version = "0.2.10"

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

View File

@ -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;
} }

View File

@ -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;
} }

View File

@ -3,7 +3,7 @@ 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::proto;
use crate::service; use crate::service;
@ -120,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);

View File

@ -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),
) )
} }

View File

@ -1,7 +1,7 @@
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;
@ -15,14 +15,14 @@ use crate::util::serde::to_socket_addrs;
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. /// Configuration version user should be using, or warning will be shown.
const CONFIG_VERSION: &str = "0.2.1"; 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;
} }
@ -63,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,
@ -101,9 +107,9 @@ pub struct Config {
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: Config = toml::from_slice(&data)?; let mut config: Config = toml::from_slice(&data)?;
// Show warning if config version is problematic // Show warning if config version is problematic
match &config.config.version { match &config.config.version {
@ -118,6 +124,7 @@ impl Config {
Ok(true) => {} Ok(true) => {}
}, },
} }
config.path.replace(path);
Ok(config) Ok(config)
} }
@ -152,8 +159,10 @@ 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,
@ -165,6 +174,11 @@ pub struct Server {
)] )]
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,
@ -173,6 +187,14 @@ pub struct Server {
#[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. /// Server starting timeout. Force kill server process if it takes longer.
#[serde(default = "u32_300")] #[serde(default = "u32_300")]
pub start_timeout: u32, pub start_timeout: u32,
@ -181,6 +203,10 @@ pub struct Server {
#[serde(default = "u32_150")] #[serde(default = "u32_150")]
pub stop_timeout: u32, 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. /// Block banned IPs as listed in banned-ips.json in server directory.
#[serde(default = "bool_true")] #[serde(default = "bool_true")]
pub block_banned_ips: bool, pub block_banned_ips: bool,
@ -194,6 +220,19 @@ pub struct Server {
pub send_proxy_v2: bool, 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.
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
#[serde(default)] #[serde(default)]
@ -444,19 +483,13 @@ impl Default for Advanced {
} }
/// Config configuration. /// Config configuration.
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize, Default)]
#[serde(default)] #[serde(default)]
pub struct ConfigConfig { pub struct ConfigConfig {
/// Configuration for lazymc version. /// Configuration for lazymc version.
pub version: Option<String>, pub version: Option<String>,
} }
impl Default for ConfigConfig {
fn default() -> Self {
Self { version: None }
}
}
fn option_pathbuf_dot() -> Option<PathBuf> { fn option_pathbuf_dot() -> Option<PathBuf> {
Some(".".into()) Some(".".into())
} }

255
src/forge.rs Normal file
View 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(())
}

View File

@ -21,6 +21,12 @@ pub async fn occupy(
) -> Result<MethodResult, ()> { ) -> Result<MethodResult, ()> {
trace!(target: "lazymc", "Using lobby method to occupy joining client"); 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 // Start lobby
lobby::serve(client, client_info, inbound, config, server, inbound_queue).await?; lobby::serve(client, client_info, inbound, config, server, inbound_queue).await?;
@ -28,3 +34,13 @@ pub async fn occupy(
Ok(MethodResult::Consumed) 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
}

View File

@ -1,41 +1,33 @@
use std::io::ErrorKind; use std::io::ErrorKind;
use std::ops::Deref; use std::ops::Deref;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Arc; use std::sync::Arc;
use std::time::Duration; use std::time::Duration;
use bytes::BytesMut; use bytes::BytesMut;
use futures::FutureExt; use futures::FutureExt;
use minecraft_protocol::data::chat::{Message, Payload};
use minecraft_protocol::decoder::Decoder; use minecraft_protocol::decoder::Decoder;
use minecraft_protocol::version::v1_14_4::handshake::Handshake; use minecraft_protocol::version::v1_14_4::login::{
use minecraft_protocol::version::v1_14_4::login::{LoginStart, LoginSuccess, SetCompression}; LoginPluginRequest, LoginPluginResponse, LoginStart, LoginSuccess, SetCompression,
use minecraft_protocol::version::v1_17_1::game::{
ClientBoundKeepAlive, ClientBoundPluginMessage, JoinGame, NamedSoundEffect,
PlayerPositionAndLook, Respawn, SetTitleSubtitle, SetTitleText, SetTitleTimes, TimeUpdate,
}; };
use nbt::CompoundTag;
use tokio::io::AsyncWriteExt; use tokio::io::AsyncWriteExt;
use tokio::net::tcp::{ReadHalf, WriteHalf}; use tokio::net::tcp::{ReadHalf, WriteHalf};
use tokio::net::TcpStream; use tokio::net::TcpStream;
use tokio::select; use tokio::select;
use tokio::time; use tokio::time;
use uuid::Uuid;
use crate::config::*; use crate::config::*;
use crate::mc; use crate::forge;
use crate::mc::uuid;
use crate::net; use crate::net;
use crate::proto; use crate::proto;
use crate::proto::client::{Client, ClientInfo, ClientState}; use crate::proto::client::{Client, ClientInfo, ClientState};
use crate::proto::packets::play::join_game::JoinGameData;
use crate::proto::{packet, packets}; use crate::proto::{packet, packets};
use crate::proxy; use crate::proxy;
use crate::server::{Server, State}; use crate::server::{Server, State};
/// Interval to send keep-alive packets at. /// Interval to send keep-alive packets at.
const KEEP_ALIVE_INTERVAL: Duration = Duration::from_secs(10); pub const KEEP_ALIVE_INTERVAL: Duration = Duration::from_secs(10);
/// Auto incrementing ID source for keep alive packets.
static KEEP_ALIVE_ID: AtomicU64 = AtomicU64::new(0);
/// Timeout for creating new server connection for lobby client. /// Timeout for creating new server connection for lobby client.
const SERVER_CONNECT_TIMEOUT: Duration = Duration::from_secs(2 * 60); const SERVER_CONNECT_TIMEOUT: Duration = Duration::from_secs(2 * 60);
@ -50,14 +42,9 @@ const SERVER_JOIN_GAME_TIMEOUT: Duration = Duration::from_secs(20);
/// ///
/// Notchian servers are slow, we must wait a little before sending play packets, because the /// 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. /// server needs time to transition the client into this state.
/// See warning at: https://wiki.vg/Protocol#Login_Success /// See warning at: <https://wiki.vg/Protocol#Login_Success>
const SERVER_WARMUP: Duration = Duration::from_secs(1); const SERVER_WARMUP: Duration = Duration::from_secs(1);
/// 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";
/// Serve lobby service for given client connection. /// Serve lobby service for given client connection.
/// ///
/// The client must be in the login state, or this will error. /// The client must be in the login state, or this will error.
@ -110,6 +97,14 @@ pub async fn serve(
debug!(target: "lazymc::lobby", "Login on lobby server (user: {})", login_start.name); 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 // Respond with set compression if compression is enabled based on threshold
if proto::COMPRESSION_THRESHOLD >= 0 { if proto::COMPRESSION_THRESHOLD >= 0 {
trace!(target: "lazymc::lobby", "Enabling compression for lobby client because server has it enabled (threshold: {})", proto::COMPRESSION_THRESHOLD); trace!(target: "lazymc::lobby", "Enabling compression for lobby client because server has it enabled (threshold: {})", proto::COMPRESSION_THRESHOLD);
@ -124,25 +119,33 @@ pub async fn serve(
trace!(target: "lazymc::lobby", "Client login success, sending required play packets for lobby world"); 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 packets to client required to get into workable play state for lobby world
send_lobby_play_packets(client, &mut writer, &server).await?; send_lobby_play_packets(client, &client_info, &mut writer, &server).await?;
// Wait for server to come online, then set up new connection to it // Wait for server to come online
stage_wait(client, &server, &config, &mut writer).await?; 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) = let (server_client, mut outbound, mut server_buf) =
connect_to_server(client_info, &inbound, &config).await?; connect_to_server(&server_client_info, &inbound, &config).await?;
let (returned_reader, returned_writer) = inbound.split(); let (returned_reader, returned_writer) = inbound.split();
reader = returned_reader; reader = returned_reader;
writer = returned_writer; writer = returned_writer;
// Grab join game packet from server // Grab join game packet from server
let join_game = let join_game_data = wait_for_server_join_game(
wait_for_server_join_game(&server_client, &mut outbound, &mut server_buf).await?; &server_client,
&server_client_info,
&mut outbound,
&mut server_buf,
)
.await?;
// Reset lobby title // Reset lobby title
send_lobby_title(client, &mut writer, "").await?; packets::play::title::send(client, &client_info, &mut writer, "").await?;
// Play ready sound if configured // Play ready sound if configured
play_lobby_ready_sound(client, &mut writer, &config).await?; play_lobby_ready_sound(client, &client_info, &mut writer, &config).await?;
// Wait a second because Notchian servers are slow // Wait a second because Notchian servers are slow
// See: https://wiki.vg/Protocol#Login_Success // See: https://wiki.vg/Protocol#Login_Success
@ -150,7 +153,8 @@ pub async fn serve(
time::sleep(SERVER_WARMUP).await; time::sleep(SERVER_WARMUP).await;
// Send respawn packet, initiates teleport to real server world // Send respawn packet, initiates teleport to real server world
send_respawn_from_join(client, &mut writer, join_game).await?; packets::play::respawn::lobby_send(client, &client_info, &mut writer, join_game_data)
.await?;
// Drain inbound connection so we don't confuse the server // Drain inbound connection so we don't confuse the server
// TODO: can we void everything? we might need to forward everything to server except // TODO: can we void everything? we might need to forward everything to server except
@ -165,11 +169,8 @@ pub async fn serve(
return Ok(()); return Ok(());
} }
// TODO: when receiving Login Plugin Request, respond with empty payload
// See: https://wiki.vg/Protocol#Login_Plugin_Request
// 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: 0x{:02X} ({})", packet.id, packet.id); debug!(target: "lazymc", "- Packet ID: 0x{:02X} ({})", packet.id, packet.id);
} }
@ -198,10 +199,7 @@ async fn respond_login_success(
) -> Result<(), ()> { ) -> Result<(), ()> {
packet::write_packet( packet::write_packet(
LoginSuccess { LoginSuccess {
uuid: Uuid::new_v3( uuid: uuid::offline_player_uuid(&login_start.name),
&Uuid::new_v3(&Uuid::nil(), b"OfflinePlayer"),
login_start.name.as_bytes(),
),
username: login_start.name.clone(), username: login_start.name.clone(),
}, },
client, client,
@ -213,6 +211,7 @@ async fn respond_login_success(
/// Play lobby ready sound effect if configured. /// Play lobby ready sound effect if configured.
async fn play_lobby_ready_sound( async fn play_lobby_ready_sound(
client: &Client, client: &Client,
client_info: &ClientInfo,
writer: &mut WriteHalf<'_>, writer: &mut WriteHalf<'_>,
config: &Config, config: &Config,
) -> Result<(), ()> { ) -> Result<(), ()> {
@ -224,8 +223,8 @@ async fn play_lobby_ready_sound(
} }
// Play sound effect // Play sound effect
send_lobby_player_pos(client, writer).await?; packets::play::player_pos::send(client, client_info, writer).await?;
send_lobby_sound_effect(client, writer, sound_name).await?; packets::play::sound::send(client, client_info, writer, sound_name).await?;
} }
Ok(()) Ok(())
@ -234,245 +233,33 @@ async fn play_lobby_ready_sound(
/// Send packets to client to get workable play state for lobby world. /// Send packets to client to get workable play state for lobby world.
async fn send_lobby_play_packets( async fn send_lobby_play_packets(
client: &Client, client: &Client,
client_info: &ClientInfo,
writer: &mut WriteHalf<'_>, writer: &mut WriteHalf<'_>,
server: &Server, server: &Server,
) -> Result<(), ()> { ) -> Result<(), ()> {
// See: https://wiki.vg/Protocol_FAQ#What.27s_the_normal_login_sequence_for_a_client.3F // See: https://wiki.vg/Protocol_FAQ#What.27s_the_normal_login_sequence_for_a_client.3F
// Send initial game join // Send initial game join
send_lobby_join_game(client, writer, server).await?; packets::play::join_game::lobby_send(client, client_info, writer, server).await?;
// Send server brand // Send server brand
send_lobby_brand(client, writer).await?; packets::play::server_brand::send(client, client_info, writer).await?;
// Send spawn and player position, disables 'download terrain' screen // Send spawn and player position, disables 'download terrain' screen
send_lobby_player_pos(client, writer).await?; packets::play::player_pos::send(client, client_info, writer).await?;
// Notify client of world time, required once before keep-alive packets // Notify client of world time, required once before keep-alive packets
send_lobby_time_update(client, writer).await?; packets::play::time_update::send(client, client_info, writer).await?;
Ok(()) Ok(())
} }
/// Send initial join game packet to client for lobby.
async fn send_lobby_join_game(
client: &Client,
writer: &mut WriteHalf<'_>,
server: &Server,
) -> Result<(), ()> {
// Send Minecrafts default states, slightly customised for lobby world
packet::write_packet(
{
let status = server.status().await;
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,
// TODO: use real server value
hardcore: false,
game_mode: 3,
previous_game_mode: -1i8 as u8,
world_names: vec![
"minecraft:overworld".into(),
"minecraft:the_nether".into(),
"minecraft:the_end".into(),
],
dimension_codec: snbt_to_compound_tag(include_str!("../res/dimension_codec.snbt")),
dimension: snbt_to_compound_tag(include_str!("../res/dimension.snbt")),
world_name: "lazymc:lobby".into(),
hashed_seed: 0,
max_players: status.as_ref().map(|s| s.players.max as i32).unwrap_or(20),
// TODO: use real server value
view_distance: 10,
// TODO: use real server value
reduced_debug_info: false,
// TODO: use real server value
enable_respawn_screen: true,
is_debug: true,
is_flat: false,
}
},
client,
writer,
)
.await
}
/// Send lobby brand to client.
async fn send_lobby_brand(client: &Client, writer: &mut WriteHalf<'_>) -> Result<(), ()> {
packet::write_packet(
ClientBoundPluginMessage {
channel: "minecraft:brand".into(),
data: SERVER_BRAND.into(),
},
client,
writer,
)
.await
}
/// Send lobby player position to client.
async fn send_lobby_player_pos(client: &Client, writer: &mut WriteHalf<'_>) -> Result<(), ()> {
// Send player location, disables download terrain screen
packet::write_packet(
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
}
/// Send lobby time update to client.
async fn send_lobby_time_update(client: &Client, writer: &mut WriteHalf<'_>) -> Result<(), ()> {
const MC_TIME_NOON: i64 = 6000;
// Send time update, required once for keep-alive packets
packet::write_packet(
TimeUpdate {
world_age: MC_TIME_NOON,
time_of_day: MC_TIME_NOON,
},
client,
writer,
)
.await
}
/// Send keep alive packet to client.
///
/// Required periodically in play mode to prevent client timeout.
async fn send_keep_alive(client: &Client, writer: &mut WriteHalf<'_>) -> Result<(), ()> {
packet::write_packet(
ClientBoundKeepAlive {
// Keep sending new IDs
id: KEEP_ALIVE_ID.fetch_add(1, Ordering::Relaxed),
},
client,
writer,
)
.await
// TODO: verify we receive keep alive response with same ID from client
}
/// 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.
async fn send_lobby_title(
client: &Client,
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");
// 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.fandom.com/wiki/Commands/title#Detail
SetTitleTimes {
fade_in: 10,
stay: 70,
fade_out: 20,
}
} else {
SetTitleTimes {
fade_in: 0,
stay: KEEP_ALIVE_INTERVAL.as_secs() as i32 * mc::TICKS_PER_SECOND as i32 * 2,
fade_out: 0,
}
},
client,
writer,
)
.await
}
/// Send lobby ready sound effect to client.
async fn send_lobby_sound_effect(
client: &Client,
writer: &mut WriteHalf<'_>,
sound_name: &str,
) -> Result<(), ()> {
packet::write_packet(
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
}
/// 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.
async fn send_respawn_from_join(
client: &Client,
writer: &mut WriteHalf<'_>,
join_game: JoinGame,
) -> Result<(), ()> {
packet::write_packet(
Respawn {
dimension: join_game.dimension,
world_name: join_game.world_name,
hashed_seed: join_game.hashed_seed,
game_mode: join_game.game_mode,
previous_game_mode: join_game.previous_game_mode,
is_debug: join_game.is_debug,
is_flat: join_game.is_flat,
copy_metadata: false,
},
client,
writer,
)
.await
}
/// An infinite keep-alive loop. /// An infinite keep-alive loop.
/// ///
/// This will keep sending keep-alive and title packets to the client until it is dropped. /// This will keep sending keep-alive and title packets to the client until it is dropped.
async fn keep_alive_loop( async fn keep_alive_loop(
client: &Client, client: &Client,
client_info: &ClientInfo,
writer: &mut WriteHalf<'_>, writer: &mut WriteHalf<'_>,
config: &Config, config: &Config,
) -> Result<(), ()> { ) -> Result<(), ()> {
@ -484,8 +271,10 @@ async fn keep_alive_loop(
trace!(target: "lazymc::lobby", "Sending keep-alive sequence to lobby client"); trace!(target: "lazymc::lobby", "Sending keep-alive sequence to lobby client");
// Send keep alive and title packets // Send keep alive and title packets
send_keep_alive(client, writer).await?; packets::play::keep_alive::send(client, client_info, writer).await?;
send_lobby_title(client, writer, &config.join.lobby.message).await?; packets::play::title::send(client, client_info, writer, &config.join.lobby.message).await?;
// TODO: verify we receive correct keep alive response
} }
} }
@ -496,12 +285,13 @@ async fn keep_alive_loop(
/// During this stage we keep sending keep-alive and title packets to the client to keep it active. /// During this stage we keep sending keep-alive and title packets to the client to keep it active.
async fn stage_wait( async fn stage_wait(
client: &Client, client: &Client,
client_info: &ClientInfo,
server: &Server, server: &Server,
config: &Config, config: &Config,
writer: &mut WriteHalf<'_>, writer: &mut WriteHalf<'_>,
) -> Result<(), ()> { ) -> Result<(), ()> {
select! { select! {
a = keep_alive_loop(client, writer, config) => a, a = keep_alive_loop(client, client_info, writer, config) => a,
b = wait_for_server(server, config) => b, b = wait_for_server(server, config) => b,
} }
} }
@ -565,7 +355,7 @@ async fn wait_for_server(server: &Server, config: &Config) -> Result<(), ()> {
/// ///
/// This will initialize the connection to the play state. Client details are used. /// This will initialize the connection to the play state. Client details are used.
async fn connect_to_server( async fn connect_to_server(
client_info: ClientInfo, client_info: &ClientInfo,
inbound: &TcpStream, inbound: &TcpStream,
config: &Config, config: &Config,
) -> Result<(Client, TcpStream, BytesMut), ()> { ) -> Result<(Client, TcpStream, BytesMut), ()> {
@ -584,7 +374,7 @@ async fn connect_to_server(
/// This will initialize the connection to the play state. Client details are used. /// This will initialize the connection to the play state. Client details are used.
// TODO: clean this up // TODO: clean this up
async fn connect_to_server_no_timeout( async fn connect_to_server_no_timeout(
client_info: ClientInfo, client_info: &ClientInfo,
inbound: &TcpStream, inbound: &TcpStream,
config: &Config, config: &Config,
) -> Result<(Client, TcpStream, BytesMut), ()> { ) -> Result<(Client, TcpStream, BytesMut), ()> {
@ -612,14 +402,14 @@ async fn connect_to_server_no_timeout(
let (mut reader, mut writer) = outbound.split(); let (mut reader, mut writer) = outbound.split();
// Handshake packet // 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( packet::write_packet(
Handshake { client_info.handshake.clone().unwrap(),
protocol_version: client_info.protocol_version.unwrap(),
server_addr: config.server.address.ip().to_string(),
server_port: config.server.address.port(),
next_state: ClientState::Login.to_id(),
},
&tmp_client, &tmp_client,
&mut writer, &mut writer,
) )
@ -628,7 +418,7 @@ async fn connect_to_server_no_timeout(
// Request login start // Request login start
packet::write_packet( packet::write_packet(
LoginStart { LoginStart {
name: client_info.username.ok_or(())?, name: client_info.username.clone().ok_or(())?,
}, },
&tmp_client, &tmp_client,
&mut writer, &mut writer,
@ -657,9 +447,7 @@ async fn connect_to_server_no_timeout(
{ {
// Decode compression packet // Decode compression packet
let set_compression = let set_compression =
SetCompression::decode(&mut packet.data.as_slice()).map_err(|err| { SetCompression::decode(&mut packet.data.as_slice()).map_err(|_| ())?;
dbg!(err);
})?;
// Client and server compression threshold should match, show warning if not // Client and server compression threshold should match, show warning if not
if set_compression.threshold != proto::COMPRESSION_THRESHOLD { if set_compression.threshold != proto::COMPRESSION_THRESHOLD {
@ -676,9 +464,59 @@ async fn connect_to_server_no_timeout(
continue; 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 // Hijack login success
if client_state == ClientState::Login && packet.id == packets::login::CLIENT_LOGIN_SUCCESS { if client_state == ClientState::Login && packet.id == packets::login::CLIENT_LOGIN_SUCCESS {
trace!(target: "lazymc::lobby", "Received login success from server connection, change to play mode"); trace!(target: "lazymc::lobby", "Got login success from server connection, change to play mode");
// TODO: parse this packet to ensure it's fine // TODO: parse this packet to ensure it's fine
// let login_success = // let login_success =
@ -698,8 +536,23 @@ async fn connect_to_server_no_timeout(
return Ok((tmp_client, outbound, buf)); 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 // Show unhandled packet warning
debug!(target: "lazymc::lobby", "Received unhandled packet from server in connect_to_server:"); debug!(target: "lazymc::lobby", "Got unhandled packet from server in connect_to_server:");
debug!(target: "lazymc::lobby", "- State: {:?}", client_state); debug!(target: "lazymc::lobby", "- State: {:?}", client_state);
debug!(target: "lazymc::lobby", "- Packet ID: 0x{:02X} ({})", packet.id, packet.id); debug!(target: "lazymc::lobby", "- Packet ID: 0x{:02X} ({})", packet.id, packet.id);
} }
@ -715,12 +568,13 @@ async fn connect_to_server_no_timeout(
/// This parses, consumes and returns the packet. /// This parses, consumes and returns the packet.
async fn wait_for_server_join_game( async fn wait_for_server_join_game(
client: &Client, client: &Client,
client_info: &ClientInfo,
outbound: &mut TcpStream, outbound: &mut TcpStream,
buf: &mut BytesMut, buf: &mut BytesMut,
) -> Result<JoinGame, ()> { ) -> Result<JoinGameData, ()> {
time::timeout( time::timeout(
SERVER_JOIN_GAME_TIMEOUT, SERVER_JOIN_GAME_TIMEOUT,
wait_for_server_join_game_no_timeout(client, outbound, buf), wait_for_server_join_game_no_timeout(client, client_info, outbound, buf),
) )
.await .await
.map_err(|_| { .map_err(|_| {
@ -735,9 +589,10 @@ async fn wait_for_server_join_game(
// TODO: do not drop error here, return Box<dyn Error> // TODO: do not drop error here, return Box<dyn Error>
async fn wait_for_server_join_game_no_timeout( async fn wait_for_server_join_game_no_timeout(
client: &Client, client: &Client,
client_info: &ClientInfo,
outbound: &mut TcpStream, outbound: &mut TcpStream,
buf: &mut BytesMut, buf: &mut BytesMut,
) -> Result<JoinGame, ()> { ) -> Result<JoinGameData, ()> {
let (mut reader, mut _writer) = outbound.split(); let (mut reader, mut _writer) = outbound.split();
loop { loop {
@ -752,16 +607,17 @@ async fn wait_for_server_join_game_no_timeout(
}; };
// Catch join game // Catch join game
if packet.id == packets::play::CLIENT_JOIN_GAME { if packets::play::join_game::is_packet(client_info, packet.id) {
let join_game = JoinGame::decode(&mut packet.data.as_slice()).map_err(|err| { // Parse join game data
dbg!(err); 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); return Ok(join_game_data);
} }
// Show unhandled packet warning // Show unhandled packet warning
debug!(target: "lazymc::lobby", "Received unhandled packet from server in wait_for_server_join_game:"); 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); debug!(target: "lazymc::lobby", "- Packet ID: 0x{:02X} ({})", packet.id, packet.id);
} }
@ -806,21 +662,3 @@ async fn drain_stream(reader: &mut ReadHalf<'_>) -> Result<(), ()> {
} }
} }
} }
/// Read NBT CompoundTag from SNBT.
fn snbt_to_compound_tag(data: &str) -> CompoundTag {
use nbt::decode::read_compound_tag;
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
read_compound_tag(&mut &*binary).unwrap()
}

View File

@ -10,6 +10,7 @@ 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; pub(crate) mod join;
#[cfg(feature = "lobby")] #[cfg(feature = "lobby")]
pub(crate) mod lobby; pub(crate) mod lobby;
@ -17,6 +18,7 @@ pub(crate) mod mc;
pub(crate) mod monitor; pub(crate) mod monitor;
pub(crate) mod net; 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;
@ -27,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";
@ -57,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

112
src/mc/dimension.rs Normal file
View 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
View 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)
}

View File

@ -1,7 +1,13 @@
pub mod ban; 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. /// Minecraft ticks per second.
#[allow(unused)] #[allow(unused)]

View File

@ -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
View 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
View 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())
}

View File

@ -57,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");
} }

View File

@ -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
}
};
}

View File

@ -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
View 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(())
}

View File

@ -2,6 +2,8 @@ use std::net::SocketAddr;
use std::sync::atomic::{AtomicI32, Ordering}; use std::sync::atomic::{AtomicI32, Ordering};
use std::sync::Mutex; use std::sync::Mutex;
use minecraft_protocol::version::v1_14_4::handshake::Handshake;
/// Client state. /// Client state.
/// ///
/// Note: this does not keep track of encryption states. /// Note: this does not keep track of encryption states.
@ -113,8 +115,11 @@ impl Default for ClientState {
/// Client info, useful during connection handling. /// Client info, useful during connection handling.
#[derive(Debug, Clone, Default)] #[derive(Debug, Clone, Default)]
pub struct ClientInfo { pub struct ClientInfo {
/// Client protocol version. /// Used protocol version.
pub protocol_version: Option<i32>, pub protocol: Option<u32>,
/// Handshake as received from client.
pub handshake: Option<Handshake>,
/// Client username. /// Client username.
pub username: Option<String>, pub username: Option<String>,
@ -124,4 +129,10 @@ impl ClientInfo {
pub fn empty() -> Self { pub fn empty() -> Self {
Self::default() 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))
}
} }

View File

@ -9,7 +9,7 @@ pub mod packets;
/// in the configuration. /// in the configuration.
/// ///
/// Should be kept up-to-date with latest supported Minecraft version by lazymc. /// Should be kept up-to-date with latest supported Minecraft version by lazymc.
pub const PROTO_DEFAULT_VERSION: &str = "1.17.1"; pub const PROTO_DEFAULT_VERSION: &str = "1.19.3";
/// Default minecraft protocol version. /// Default minecraft protocol version.
/// ///
@ -17,7 +17,7 @@ pub const PROTO_DEFAULT_VERSION: &str = "1.17.1";
/// in the configuration. /// in the configuration.
/// ///
/// Should be kept up-to-date with latest supported Minecraft version by lazymc. /// Should be kept up-to-date with latest supported Minecraft version by lazymc.
pub const PROTO_DEFAULT_PROTOCOL: u32 = 756; pub const PROTO_DEFAULT_PROTOCOL: u32 = 761;
/// Compression threshold to use. /// Compression threshold to use.
// TODO: read this from server.properties instead // TODO: read this from server.properties instead

View File

@ -1,3 +1,4 @@
use std::fmt::Debug;
use std::io::prelude::*; use std::io::prelude::*;
use bytes::BytesMut; use bytes::BytesMut;
@ -44,11 +45,22 @@ impl RawPacket {
/// ///
/// This decodes both compressed and uncompressed packets based on the client threshold /// This decodes both compressed and uncompressed packets based on the client threshold
/// preference. /// preference.
pub fn decode(client: &Client, mut buf: &[u8]) -> Result<Self, ()> { pub fn decode_with_len(client: &Client, mut buf: &[u8]) -> Result<Self, ()> {
// Read length // Read length
let (read, len) = types::read_var_int(buf)?; let (read, len) = types::read_var_int(buf)?;
buf = &buf[read..][..len as usize]; 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 no compression is used, read remaining packet ID and data
if !client.is_compressed() { if !client.is_compressed() {
// Read packet ID and data // Read packet ID and data
@ -85,7 +97,20 @@ impl RawPacket {
/// Encode packet to raw buffer. /// Encode packet to raw buffer.
/// ///
/// This compresses packets based on the client threshold preference. /// This compresses packets based on the client threshold preference.
pub fn encode(&self, client: &Client) -> Result<Vec<u8>, ()> { 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(); let threshold = client.compressed();
if threshold >= 0 { if threshold >= 0 {
self.encode_compressed(threshold) self.encode_compressed(threshold)
@ -103,8 +128,7 @@ impl RawPacket {
// Determine whether to compress, encode data length bytes // Determine whether to compress, encode data length bytes
let data_len = payload.len() as i32; let data_len = payload.len() as i32;
let compress = data_len > threshold; let compress = data_len > threshold;
let mut data_len_bytes = let data_len_header = if compress { data_len } else { 0 };
types::encode_var_int(if compress { data_len } else { 0 }).unwrap();
// Compress payload // Compress payload
if compress { if compress {
@ -117,10 +141,8 @@ impl RawPacket {
})?; })?;
} }
// Encapsulate payload with packet and data length // Add data length header
let len = data_len_bytes.len() as i32 + payload.len() as i32; let mut packet = types::encode_var_int(data_len_header).unwrap();
let mut packet = types::encode_var_int(len)?;
packet.append(&mut data_len_bytes);
packet.append(&mut payload); packet.append(&mut payload);
Ok(packet) Ok(packet)
@ -128,12 +150,8 @@ impl RawPacket {
/// Encode uncompressed packet to raw buffer. /// Encode uncompressed packet to raw buffer.
fn encode_uncompressed(&self) -> Result<Vec<u8>, ()> { fn encode_uncompressed(&self) -> Result<Vec<u8>, ()> {
let mut data = types::encode_var_int(self.id as i32)?; let mut packet = types::encode_var_int(self.id as i32)?;
data.extend_from_slice(&self.data); packet.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) Ok(packet)
} }
@ -194,22 +212,23 @@ pub async fn read_packet(
} }
// Parse packet, use full buffer since we'll read the packet length again // 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 raw = buf.split_to(consumed + len as usize);
let packet = RawPacket::decode(client, &raw)?; let packet = RawPacket::decode_with_len(client, &raw)?;
Ok(Some((packet, raw.to_vec()))) Ok(Some((packet, raw.to_vec())))
} }
/// Write packet to stream writer. /// Write packet to stream writer.
pub async fn write_packet( pub async fn write_packet(
packet: impl PacketId + Encoder, packet: impl PacketId + Encoder + Debug,
client: &Client, client: &Client,
writer: &mut WriteHalf<'_>, writer: &mut WriteHalf<'_>,
) -> Result<(), ()> { ) -> Result<(), ()> {
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(packet.packet_id(), data).encode(client)?; let response = RawPacket::new(packet.packet_id(), data).encode_with_len(client)?;
writer.write_all(&response).await.map_err(|_| ())?; writer.write_all(&response).await.map_err(|_| ())?;
Ok(()) Ok(())

View File

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

33
src/proto/packets/mod.rs Normal file
View 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;
}

View 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
}
}
}

View 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,
}
}

View 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;

View 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
}
}
}

View 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
}
}
}

View 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
}
}
}

View 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
}
}
}

View 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
}
}
}

View 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
}

View File

@ -100,6 +100,7 @@ pub enum ProxyHeader {
None, None,
/// Header for locally initiated connection. /// Header for locally initiated connection.
#[allow(unused)]
Local, Local,
/// Header for proxied connection. /// Header for proxied connection.

View File

@ -12,9 +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::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.
@ -27,6 +29,12 @@ 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);
/// Exit codes that are allowed.
///
/// - 143: https://github.com/timvisee/lazymc/issues/26#issuecomment-1435670029
/// - 130: https://unix.stackexchange.com/q/386836/61092
const ALLOWED_EXIT_CODES: [i32; 2] = [130, 143];
/// Shared server state. /// Shared server state.
#[derive(Debug)] #[derive(Debug)]
pub struct Server { pub struct Server {
@ -68,6 +76,9 @@ pub struct Server {
/// List of banned IPs. /// List of banned IPs.
banned_ips: RwLock<BannedIps>, 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,
@ -75,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 {
@ -200,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
} }
@ -218,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 {
@ -333,6 +363,18 @@ impl Server {
futures::executor::block_on(async { self.is_banned_ip(ip).await }) 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. /// Update the list of banned IPs.
pub async fn set_banned_ips(&self, ips: BannedIps) { pub async fn set_banned_ips(&self, ips: BannedIps) {
*self.banned_ips.write().await = ips; *self.banned_ips.write().await = ips;
@ -342,6 +384,16 @@ impl Server {
pub fn set_banned_ips_blocking(&self, ips: BannedIps) { pub fn set_banned_ips_blocking(&self, ips: BannedIps) {
futures::executor::block_on(async { self.set_banned_ips(ips).await }) 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 {
@ -358,10 +410,13 @@ impl Default for Server {
keep_online_until: Default::default(), keep_online_until: Default::default(),
kill_at: Default::default(), kill_at: Default::default(),
banned_ips: 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(),
} }
} }
} }
@ -417,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);
} }
@ -443,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
@ -499,7 +563,7 @@ async fn stop_server_rcon(config: &Config, server: &Server) -> bool {
} }
// Create RCON client // Create RCON client
let mut rcon = match Rcon::connect_config(&config).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);
@ -539,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;
@ -555,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
}

View File

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

172
src/service/file_watcher.rs Normal file
View 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);
}
}
}

View File

@ -1,4 +1,5 @@
pub mod ban_reload; 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
View 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
}

View File

@ -46,19 +46,22 @@ pub async fn service(config: Arc<Config>) -> Result<(), ()> {
); );
} }
// Spawn server monitor and signal handler services // 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()));
tokio::task::spawn_blocking({
let (config, server) = (config.clone(), server.clone());
|| service::ban_reload::service(config, server)
});
// Initiate server start // Initiate server start
if config.server.wake_on_start { if config.server.wake_on_start {
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());
@ -81,7 +84,7 @@ fn route(inbound: TcpStream, config: Arc<Config>, server: Arc<Server>) {
// Check ban state, just drop connection if enabled // Check ban state, just drop connection if enabled
let banned = server.is_banned_ip_blocking(&peer.ip()); let banned = server.is_banned_ip_blocking(&peer.ip());
if config.server.drop_banned_ips { if banned && config.server.drop_banned_ips {
info!(target: "lazymc", "Connection from banned IP {}, dropping", peer.ip()); info!(target: "lazymc", "Connection from banned IP {}, dropping", peer.ip());
return; return;
} }

View File

@ -12,8 +12,9 @@ use tokio::fs;
use tokio::io::AsyncWriteExt; use tokio::io::AsyncWriteExt;
use tokio::net::TcpStream; use tokio::net::TcpStream;
use crate::config::*; use crate::config::{Config, Server as ConfigServer};
use crate::join; use crate::join;
use crate::mc::favicon;
use crate::proto::action; use crate::proto::action;
use crate::proto::client::{Client, ClientInfo, ClientState}; use crate::proto::client::{Client, ClientInfo, ClientState};
use crate::proto::packet::{self, RawPacket}; use crate::proto::packet::{self, RawPacket};
@ -26,6 +27,9 @@ const BAN_MESSAGE_PREFIX: &str = "Your IP address is banned from this server.\nR
/// Default ban reason if unknown. /// Default ban reason if unknown.
const DEFAULT_BAN_REASON: &str = "Banned by an operator."; 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. /// Server icon file path.
const SERVER_ICON_FILE: &str = "server-icon.png"; const SERVER_ICON_FILE: &str = "server-icon.png";
@ -84,8 +88,9 @@ pub async fn serve(
// Update client info and client state // Update client info and client state
client_info client_info
.protocol_version .protocol
.replace(handshake.protocol_version); .replace(handshake.protocol_version as u32);
client_info.handshake.replace(handshake);
client.set_state(new_state); client.set_state(new_state);
// If loggin in with handshake, remember inbound // If loggin in with handshake, remember inbound
@ -98,13 +103,13 @@ pub async fn serve(
// Hijack server status packet // Hijack server status packet
if client_state == ClientState::Status && packet.id == packets::status::SERVER_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(&client)?; 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;
@ -147,12 +152,17 @@ pub async fn serve(
info!(target: "lazymc", "Login from banned IP {}, disconnecting", client.peer.ip()); info!(target: "lazymc", "Login from banned IP {}, disconnecting", client.peer.ip());
DEFAULT_BAN_REASON.to_string() DEFAULT_BAN_REASON.to_string()
}; };
action::kick( action::kick(&client, &format!("{BAN_MESSAGE_PREFIX}{msg}"), &mut writer)
&client, .await?;
&format!("{}{}", BAN_MESSAGE_PREFIX, msg), break;
&mut writer, }
) }
.await?;
// 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; break;
} }
} }
@ -187,7 +197,7 @@ pub async fn serve(
} }
// 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);
} }
@ -196,7 +206,7 @@ pub async fn serve(
} }
/// 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(); let server_state = server.state();
@ -230,12 +240,16 @@ async fn server_status(config: &Config, server: &Server) -> ServerStatus {
} }
}; };
// Get server favicon // Extract favicon from real server status, load from disk, or use default
let favicon = if config.motd.from_server && status.is_some() { let mut favicon = None;
status.as_ref().unwrap().favicon.clone() if favicon::supports_favicon(client_info) {
} else { if config.motd.from_server && status.is_some() {
favicon(&config).await 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 {
@ -251,31 +265,31 @@ async fn server_status(config: &Config, server: &Server) -> ServerStatus {
} }
/// Get server status favicon. /// Get server status favicon.
async fn favicon(config: &Config) -> Option<String> { ///
/// This always returns a favicon, returning the default one if none is set.
async fn server_favicon(config: &Config) -> String {
// Get server dir // Get server dir
let dir = match config.server.directory.as_ref() { let dir = match ConfigServer::server_directory(config) {
Some(dir) => dir, Some(dir) => dir,
None => return None, None => return favicon::default_favicon(),
}; };
// Server icon file, ensure it exists // Server icon file, ensure it exists
let path = dir.join(SERVER_ICON_FILE); let path = dir.join(SERVER_ICON_FILE);
if !path.is_file() { if !path.is_file() {
return None; return favicon::default_favicon();
} }
// Read icon data // Read icon data
let data = fs::read(path) let data = match fs::read(path).await.map_err(|err| {
.await error!(target: "lazymc", "Failed to read favicon from {}: {}", SERVER_ICON_FILE, err);
.map_err(|err| { }) {
error!(target: "lazymc", "Failed to read favicon from {}: {}", SERVER_ICON_FILE, err); Ok(data) => data,
}) Err(err) => {
.ok()?; error!(target: "lazymc::status", "Failed to load server icon from disk, using default: {:?}", err);
return favicon::default_favicon();
}
};
// Format and return favicon favicon::encode_favicon(&data)
Some(format!(
"{}{}",
"data:image/png;base64,",
base64::encode(data)
))
} }

View File

@ -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() {

View File

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