Compare commits
No commits in common. "master" and "v0.2.4" have entirely different histories.
135
.gitlab-ci.yml
135
.gitlab-ci.yml
@ -9,16 +9,18 @@ stages:
|
||||
|
||||
# Variable defaults
|
||||
variables:
|
||||
RUST_VERSION: stable
|
||||
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
|
||||
before_script:
|
||||
- apt-get update
|
||||
- apt-get install -y --no-install-recommends build-essential
|
||||
- |
|
||||
rustup install $RUST_VERSION
|
||||
rustup default $RUST_VERSION
|
||||
- |
|
||||
rustc --version
|
||||
cargo --version
|
||||
@ -27,39 +29,54 @@ before_script:
|
||||
.before_script-windows: &before_script-windows
|
||||
before_script:
|
||||
# Install scoop
|
||||
- iex "& {$(irm get.scoop.sh)} -RunAsAdmin"
|
||||
- Invoke-Expression (New-Object System.Net.WebClient).DownloadString('https://get.scoop.sh')
|
||||
|
||||
# Install Rust
|
||||
- scoop install rustup gcc
|
||||
- rustup install $RUST_VERSION
|
||||
- rustup default $RUST_VERSION
|
||||
- scoop install rustup
|
||||
- rustc --version
|
||||
- cargo --version
|
||||
|
||||
# Install proper Rust target
|
||||
- rustup target install x86_64-pc-windows-msvc
|
||||
|
||||
# Check on stable, beta and nightly
|
||||
.check-base: &check-base
|
||||
stage: check
|
||||
cache:
|
||||
<<: *rust-build-cache
|
||||
script:
|
||||
- cargo check --verbose
|
||||
- cargo check --no-default-features --verbose
|
||||
- cargo check --no-default-features --features rcon --verbose
|
||||
- cargo check --no-default-features --features lobby --verbose
|
||||
check-stable:
|
||||
check:
|
||||
<<: *check-base
|
||||
check-msrv:
|
||||
<<: *check-base
|
||||
variables:
|
||||
RUST_VERSION: 1.64.0
|
||||
check-macos:
|
||||
tags:
|
||||
- macos
|
||||
only:
|
||||
- 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-x86_64-linux-gnu:
|
||||
stage: build
|
||||
needs: []
|
||||
cache:
|
||||
<<: *rust-build-cache
|
||||
script:
|
||||
- cargo build --target=$TARGET --release --locked --verbose
|
||||
- mv target/$TARGET/release/lazymc ./lazymc-$TARGET
|
||||
@ -73,12 +90,11 @@ build-x86_64-linux-gnu:
|
||||
# Build a static version
|
||||
build-x86_64-linux-musl:
|
||||
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: []
|
||||
variables:
|
||||
TARGET: x86_64-unknown-linux-musl
|
||||
cache:
|
||||
<<: *rust-build-cache
|
||||
script:
|
||||
- rustup target add $TARGET
|
||||
- cargo build --target=$TARGET --release --locked --verbose
|
||||
@ -97,12 +113,11 @@ build-x86_64-linux-musl:
|
||||
build-armv7-linux-gnu:
|
||||
stage: build
|
||||
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: []
|
||||
variables:
|
||||
TARGET: armv7-unknown-linux-gnueabihf
|
||||
cache:
|
||||
<<: *rust-build-cache
|
||||
before_script:
|
||||
- apt-get update
|
||||
- apt-get install -y --no-install-recommends build-essential
|
||||
@ -133,12 +148,11 @@ build-armv7-linux-gnu:
|
||||
build-aarch64-linux-gnu:
|
||||
stage: build
|
||||
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: []
|
||||
variables:
|
||||
TARGET: aarch64-unknown-linux-gnu
|
||||
cache:
|
||||
<<: *rust-build-cache
|
||||
before_script:
|
||||
- apt-get update
|
||||
- apt-get install -y --no-install-recommends build-essential
|
||||
@ -165,14 +179,36 @@ build-aarch64-linux-gnu:
|
||||
- lazymc-$TARGET
|
||||
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-x86_64-windows:
|
||||
stage: build
|
||||
tags:
|
||||
- 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: []
|
||||
variables:
|
||||
TARGET: x86_64-pc-windows-msvc
|
||||
@ -189,28 +225,29 @@ build-x86_64-windows:
|
||||
# Run the unit tests through Cargo on Linux
|
||||
test-cargo-x86_64-linux-gnu:
|
||||
stage: test
|
||||
only:
|
||||
- master
|
||||
needs: []
|
||||
dependencies: []
|
||||
cache:
|
||||
<<: *rust-build-cache
|
||||
script:
|
||||
- cargo test --locked --verbose
|
||||
- cargo test --locked --no-default-features --verbose
|
||||
- cargo test --locked --no-default-features --features rcon --verbose
|
||||
- cargo test --locked --no-default-features --features lobby --verbose
|
||||
|
||||
# # Run the unit tests through Cargo on Windows
|
||||
# test-cargo-x86_64-windows:
|
||||
# stage: test
|
||||
# tags:
|
||||
# - windows
|
||||
# needs: []
|
||||
# dependencies: []
|
||||
# <<: *before_script-windows
|
||||
# script:
|
||||
# - cargo test --locked --verbose
|
||||
# - cargo test --locked --no-default-features --features rcon --verbose
|
||||
# - cargo test --locked --no-default-features --features rcon,lobby --verbose
|
||||
# Run the unit tests through Cargo on Windows
|
||||
test-cargo-x86_64-windows:
|
||||
stage: test
|
||||
tags:
|
||||
- windows
|
||||
needs: []
|
||||
dependencies: []
|
||||
cache: {}
|
||||
<<: *before_script-windows
|
||||
script:
|
||||
- cargo test --locked --verbose
|
||||
- cargo test --locked --no-default-features --features rcon --verbose
|
||||
- cargo test --locked --no-default-features --features lobby --verbose
|
||||
|
||||
# Release binaries on GitLab as generic package
|
||||
release-gitlab-generic-package:
|
||||
@ -221,14 +258,16 @@ release-gitlab-generic-package:
|
||||
- build-x86_64-linux-musl
|
||||
- build-armv7-linux-gnu
|
||||
- build-aarch64-linux-gnu
|
||||
- build-macos
|
||||
- build-x86_64-windows
|
||||
only:
|
||||
- /^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-]+)*))?$/
|
||||
- /^v(\d+\.)*\d+$/
|
||||
variables:
|
||||
LINUX_GNU_BIN: "lazymc-x86_64-unknown-linux-gnu"
|
||||
LINUX_MUSL_BIN: "lazymc-x86_64-unknown-linux-musl"
|
||||
LINUX_ARMV7_GNU_BIN: "lazymc-armv7-unknown-linux-gnueabihf"
|
||||
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"
|
||||
before_script: []
|
||||
script:
|
||||
@ -245,6 +284,8 @@ 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_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}
|
||||
|
||||
@ -253,12 +294,13 @@ release-gitlab-release:
|
||||
image: registry.gitlab.com/gitlab-org/release-cli
|
||||
stage: release
|
||||
only:
|
||||
- /^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-]+)*))?$/
|
||||
- /^v(\d+\.)*\d+$/
|
||||
variables:
|
||||
LINUX_GNU_BIN: "lazymc-x86_64-unknown-linux-gnu"
|
||||
LINUX_MUSL_BIN: "lazymc-x86_64-unknown-linux-musl"
|
||||
LINUX_ARMV7_GNU_BIN: "lazymc-armv7-unknown-linux-gnueabihf"
|
||||
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"
|
||||
before_script: []
|
||||
script:
|
||||
@ -273,18 +315,20 @@ release-gitlab-release:
|
||||
--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_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}\"}"
|
||||
|
||||
# Publish GitHub release
|
||||
release-github:
|
||||
stage: release
|
||||
only:
|
||||
- /^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-]+)*))?$/
|
||||
- /^v(\d+\.)*\d+$/
|
||||
dependencies:
|
||||
- build-x86_64-linux-gnu
|
||||
- build-x86_64-linux-musl
|
||||
- build-armv7-linux-gnu
|
||||
- build-aarch64-linux-gnu
|
||||
- build-macos
|
||||
- build-x86_64-windows
|
||||
before_script: []
|
||||
script:
|
||||
@ -303,4 +347,5 @@ 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-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-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
|
||||
|
39
CHANGELOG.md
39
CHANGELOG.md
@ -1,44 +1,5 @@
|
||||
# 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
|
||||
|
923
Cargo.lock
generated
923
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
63
Cargo.toml
63
Cargo.toml
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "lazymc"
|
||||
version = "0.2.10"
|
||||
version = "0.2.4"
|
||||
authors = ["Tim Visee <3a4fb3964f@sinenomine.email>"]
|
||||
license = "GPL-3.0"
|
||||
readme = "README.md"
|
||||
@ -8,15 +8,16 @@ homepage = "https://timvisee.com/projects/lazymc"
|
||||
repository = "https://gitlab.com/timvisee/lazymc"
|
||||
description = "Put your Minecraft server to rest when idle."
|
||||
keywords = ["minecraft", "server", "idle", "cli"]
|
||||
categories = ["command-line-interface", "games"]
|
||||
exclude = ["/.github", "/contrib"]
|
||||
categories = [
|
||||
"command-line-interface",
|
||||
"games",
|
||||
]
|
||||
exclude = [
|
||||
"/.github",
|
||||
"/contrib",
|
||||
]
|
||||
edition = "2021"
|
||||
|
||||
[profile.release]
|
||||
codegen-units = 1
|
||||
lto = true
|
||||
strip = true
|
||||
|
||||
[features]
|
||||
default = ["rcon", "lobby"]
|
||||
|
||||
@ -27,52 +28,30 @@ rcon = ["rust_rcon", "async-std"]
|
||||
|
||||
# Lobby support
|
||||
# Add lobby join method, keeps client in fake lobby world until server is ready.
|
||||
lobby = ["md-5", "uuid"]
|
||||
lobby = ["md-5", "named-binary-tag", "quartz_nbt", "uuid"]
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0"
|
||||
base64 = "0.21"
|
||||
base64 = "0.13"
|
||||
bytes = "1.1"
|
||||
chrono = "0.4"
|
||||
clap = { version = "4.0.32", default-features = false, features = [
|
||||
"std",
|
||||
"help",
|
||||
"suggestions",
|
||||
"color",
|
||||
"usage",
|
||||
"cargo",
|
||||
"env",
|
||||
"unicode",
|
||||
] }
|
||||
clap = { version = "3.0.0-beta.5", default-features = false, features = [ "std", "cargo", "color", "env", "suggestions", "unicode" ]}
|
||||
colored = "2.0"
|
||||
derive_builder = "0.12"
|
||||
derive_builder = "0.10"
|
||||
dotenv = "0.15"
|
||||
flate2 = { version = "1.0", default-features = false, features = ["default"] }
|
||||
futures = { version = "0.3", default-features = false, features = ["executor"] }
|
||||
log = "0.4"
|
||||
minecraft-protocol = { git = "https://github.com/timvisee/rust-minecraft-protocol", rev = "edfdf87" }
|
||||
named-binary-tag = "0.6"
|
||||
nix = "0.26"
|
||||
minecraft-protocol = { git = "https://github.com/timvisee/rust-minecraft-protocol", rev = "356ea54" }
|
||||
notify = "4.0"
|
||||
pretty_env_logger = "0.4"
|
||||
proxy-protocol = "0.5"
|
||||
quartz_nbt = "0.2"
|
||||
rand = "0.8"
|
||||
serde = "1.0"
|
||||
serde_json = "1.0"
|
||||
shlex = "1.1"
|
||||
thiserror = "1.0"
|
||||
tokio = { version = "1", default-features = false, features = [
|
||||
"rt-multi-thread",
|
||||
"io-util",
|
||||
"net",
|
||||
"macros",
|
||||
"time",
|
||||
"process",
|
||||
"signal",
|
||||
"sync",
|
||||
"fs",
|
||||
] }
|
||||
tokio = { version = "1", default-features = false, features = ["rt-multi-thread", "io-util", "net", "macros", "time", "process", "signal", "sync", "fs"] }
|
||||
toml = "0.5"
|
||||
version-compare = "0.1"
|
||||
|
||||
@ -81,17 +60,13 @@ rust_rcon = { package = "rcon", version = "0.5.2", optional = true }
|
||||
async-std = { version = "1.9.0", default-features = false, optional = true }
|
||||
|
||||
# Feature: lobby
|
||||
md-5 = { version = "0.10", optional = true }
|
||||
md-5 = { version = "0.9", optional = true }
|
||||
named-binary-tag = { version = "0.6", optional = true }
|
||||
quartz_nbt = { version = "0.2", optional = true }
|
||||
uuid = { version = "0.7", optional = true, features = ["v3"] }
|
||||
|
||||
[target.'cfg(unix)'.dependencies]
|
||||
libc = "0.2"
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
winapi = { version = "0.3", features = [
|
||||
"winuser",
|
||||
"processthreadsapi",
|
||||
"handleapi",
|
||||
"ntdef",
|
||||
"minwindef",
|
||||
] }
|
||||
winapi = { version = "0.3", features = ["winuser", "processthreadsapi", "handleapi", "ntdef", "minwindef"] }
|
||||
|
15
README.md
15
README.md
@ -43,7 +43,7 @@ https://user-images.githubusercontent.com/856222/141378688-882082be-9efa-4cfe-81
|
||||
- _Lobby: keep client in emulated server with lobby world, teleport to real server when ready ([experimental*](./docs/join-method-lobby.md))_
|
||||
- Customizable MOTD and login messages
|
||||
- Automatically manages `server.properties` (host, port and RCON settings)
|
||||
- Automatically block banned IPs from server within lazymc
|
||||
- Automatically block banned IPs from server within `lazymc`
|
||||
- Graceful server sleep/shutdown through RCON or `SIGTERM`
|
||||
- Real client IP on Minecraft server with `PROXY` header ([usage](./docs/proxy-ip.md))
|
||||
- Restart server on crash
|
||||
@ -55,10 +55,6 @@ https://user-images.githubusercontent.com/856222/141378688-882082be-9efa-4cfe-81
|
||||
- Minecraft Java Edition 1.6+
|
||||
- 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
|
||||
using a Minecraft shared hosting provider with a custom dashboard, you likely
|
||||
won't be able to set this up._
|
||||
@ -71,8 +67,7 @@ _Note: these instructions are for Linux & macOS, for Windows look
|
||||
Make sure you meet all [requirements](#requirements).
|
||||
|
||||
Download the appropriate binary for your system from the [latest
|
||||
release][latest-release] page. On macOS you must [compile from
|
||||
source](#compile-from-source).
|
||||
release][latest-release] page.
|
||||
|
||||
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:
|
||||
@ -82,8 +77,8 @@ chmod a+x ./lazymc
|
||||
./lazymc --help
|
||||
```
|
||||
|
||||
When lazymc is set-up, change into your server directory if you haven't already.
|
||||
Then set up the [configuration](./res/lazymc.toml) and start it up:
|
||||
When `lazymc` is set-up, change into your server directory if you haven't
|
||||
already. Then set up the [configuration](./res/lazymc.toml) and start it up:
|
||||
|
||||
```bash
|
||||
# Change into your server directory (if you haven't already)
|
||||
@ -145,7 +140,7 @@ cargo build --release
|
||||
|
||||
## 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:
|
||||
|
||||
- Docker: [crbanman/papermc-lazymc](https://hub.docker.com/r/crbanman/papermc-lazymc) _(PaperMC with lazymc in Docker)_
|
||||
|
4
build.rs
4
build.rs
@ -1,7 +1,7 @@
|
||||
fn main() {
|
||||
// rcon is required on Windows
|
||||
// Must enable rcon on Windows
|
||||
#[cfg(all(windows, not(feature = "rcon")))]
|
||||
{
|
||||
compile_error!("required feature missing on Windows: rcon");
|
||||
println!("cargo:warning=lazymc: you must enable rcon feature on Windows");
|
||||
}
|
||||
}
|
||||
|
@ -1 +0,0 @@
|
||||
msrv = "1.64.0"
|
@ -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 +0,0 @@
|
||||
command-bash.md
|
41
docs/command_bash.md
Normal file
41
docs/command_bash.md
Normal file
@ -0,0 +1,41 @@
|
||||
# Use bash script to start server
|
||||
|
||||
You may use a `bash` script to start your server rather than invoking `java`
|
||||
directly. This requires some changes though to ensure your server properly shuts
|
||||
down.
|
||||
|
||||
When `lazymc` stops your server it sends a [`SIGTERM`][sigterm] signal to the
|
||||
invoked server process to gracefully shut it down. `bash` ignores this signal by
|
||||
default and keeps the Minecraft server running.
|
||||
|
||||
You must configure `bash` to [forward][forward-signal] the signal to properly
|
||||
shutdown the Minecraft server as well.
|
||||
|
||||
[sigterm]: https://en.wikipedia.org/wiki/Signal_(IPC)#SIGTERM
|
||||
[forward-signal]: https://unix.stackexchange.com/a/434269/61092
|
||||
|
||||
## Example
|
||||
|
||||
Here's a minimal example, trapping the signal and forwarding it to the server.
|
||||
Be sure to set the correct server JAR file and appropriate memory limits.
|
||||
|
||||
[`start-server`](../res/start-server):
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
|
||||
# Server JAR file, set this to your own
|
||||
FILE=server.jar
|
||||
|
||||
# Trap SIGTERM, forward it to server process ID
|
||||
trap 'kill -TERM $PID' TERM INT
|
||||
|
||||
# Start server
|
||||
java -Xms1G -Xmx1G -jar $FILE --nogui &
|
||||
|
||||
# Remember server process ID, wait for it to quit, then reset the trap
|
||||
PID=$!
|
||||
wait $PID
|
||||
trap - TERM INT
|
||||
wait $PID
|
||||
```
|
@ -3,7 +3,7 @@
|
||||
Some extra steps and recommendations when using lazymc:
|
||||
|
||||
Before you use this in production, always ensure starting and stopping the
|
||||
server works as expected by connecting to it once. Watch lazymc's output while
|
||||
server works as expected by connecting to it once. Watch `lazymc`s output while
|
||||
it starts and stops. If stopping results in errors, fix this first to prevent
|
||||
corrupting world/user data.
|
||||
|
||||
|
@ -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
|
||||
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
|
||||
we're waiting on the server to start.
|
||||
|
||||
@ -21,11 +21,9 @@ enable this in a production environment.
|
||||
|
||||
Current limitations:
|
||||
|
||||
- Server must be in offline mode (`online-mode=false`)
|
||||
- Server must use Minecraft version 1.16.3 to 1.17.1 (tested with 1.17.1)
|
||||
- Server must use vanilla Minecraft
|
||||
- May work with Forge (set `server.forge = true`), depends on used mods, test before use
|
||||
- Does not work with other mods, such as FTB
|
||||
- Only works with offline mode
|
||||
- Only works with vanilla Minecraft clients, does not work with modded (e.g. Forge, FTB)
|
||||
- Probably only works with Minecraft 1.16-1.17.1 (tested with 1.17.1)
|
||||
- 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,
|
||||
@ -62,18 +60,15 @@ Then configure the lobby to your likings:
|
||||
# The client will be teleported to the real server once it is ready.
|
||||
# This may keep the client occupied forever if no timeout is set.
|
||||
# Consumes client, not allowing other join methods afterwards.
|
||||
# See: https://git.io/JMIi4
|
||||
|
||||
# !!! WARNING !!!
|
||||
# This is highly experimental, incomplete and unstable.
|
||||
# This is highly experimental and unstable.
|
||||
# This may break the game and crash clients.
|
||||
# Don't enable this unless you know what you're doing.
|
||||
#
|
||||
# - Server must be in offline mode
|
||||
# - Server must use Minecraft version 1.16.3 to 1.17.1 (tested with 1.17.1)
|
||||
# - Server must use vanilla Minecraft
|
||||
# - May work with Forge, enable in config, depends on used mods, test before use
|
||||
# - Does not work with other mods, such as FTB
|
||||
# - Only works with offline mode
|
||||
# - Only works with vanilla Minecraft clients, does not work with modded
|
||||
# - Only tested with Minecraft 1.17.1
|
||||
|
||||
# Maximum time in seconds in the lobby while the server starts.
|
||||
timeout = 600
|
||||
@ -90,25 +85,3 @@ ready_sound = "block.note_block.chime"
|
||||
|
||||
_Note: this might have changed, see the latest configuration
|
||||
[here](../res/lazymc.toml)._
|
||||
|
||||
## Probe issue with whitelist
|
||||
|
||||
lazymc may report a _probe_ error on first start when a whitelist is enabled
|
||||
on your server.
|
||||
|
||||
lazymc uses a probe to fetch some required details from your Minecraft
|
||||
server, such as a mod list. When probing, the server is started once when lazymc
|
||||
starts. It then connects to the Minecraft server with the _probe_ user
|
||||
(username: `_lazymc_probe`) and disconnects when everything needed is fetched.
|
||||
|
||||
If you use a whitelist on your server it will cause issues if the probe user
|
||||
isn't whitelisted. Simply whitelist the probe user with the following command
|
||||
and restart lazymc to fix the issue:
|
||||
|
||||
```
|
||||
/whitelist add _lazymc_probe
|
||||
```
|
||||
|
||||
Probing isn't enabled by default. You may enable this by setting
|
||||
`server.probe_on_start = true`. Other configuration settings might
|
||||
automatically enable proving if required for your setup.
|
||||
|
@ -10,7 +10,7 @@ gets a new protocol version.
|
||||
|
||||
## 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):
|
||||
|
||||
@ -21,8 +21,8 @@ In lazymc you may configure what protocol version to use:
|
||||
# Server version & protocol hint.
|
||||
# Sent to clients until actual server version is known.
|
||||
# See: https://git.io/J1Fvx
|
||||
version = "1.19.3"
|
||||
protocol = 761
|
||||
version = "1.17.1"
|
||||
protocol = 756
|
||||
|
||||
# -- 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
|
||||
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.
|
||||
|
@ -14,8 +14,8 @@ Open a terminal, go to the server directory, and make sure you can execute it:
|
||||
.\lazymc --help
|
||||
```
|
||||
|
||||
When lazymc is ready, set up the [configuration](../res/lazymc.toml) and start it
|
||||
up:
|
||||
When `lazymc` is ready, set up the [configuration](./res/lazymc.toml) and start
|
||||
it up:
|
||||
|
||||
```bash
|
||||
# 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
|
||||
[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
|
||||
|
@ -18,8 +18,8 @@
|
||||
# Server version & protocol hint.
|
||||
# Sent to clients until actual server version is known.
|
||||
# See: https://git.io/J1Fvx
|
||||
#version = "1.19.3"
|
||||
#protocol = 761
|
||||
#version = "1.17.1"
|
||||
#protocol = 756
|
||||
|
||||
[server]
|
||||
# Server address. Internal IP and port of server started by lazymc to proxy to.
|
||||
@ -30,33 +30,19 @@
|
||||
directory = "."
|
||||
|
||||
# Command to start the server.
|
||||
# Warning: if using a bash script read: https://git.io/JMIKH
|
||||
# Warning: if using a bash script read: https://git.io/J1FvZ
|
||||
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.
|
||||
#wake_on_start = false
|
||||
|
||||
# Immediately wake server after crash.
|
||||
#wake_on_crash = false
|
||||
|
||||
# Probe required server details when starting lazymc, wakes server on start.
|
||||
# Improves client compatibility. Automatically enabled if required by other config properties.
|
||||
#probe_on_start = false
|
||||
|
||||
# Set to true if this server runs Forge.
|
||||
#forge = false
|
||||
|
||||
# Server start/stop timeout in seconds. Force kill server process if it takes too long.
|
||||
#start_timeout = 300
|
||||
#stop_timeout = 150
|
||||
|
||||
# To wake server, user must be in server whitelist if enabled on server.
|
||||
#wake_whitelist = true
|
||||
|
||||
# Block banned IPs as listed in banned-ips.json in server directory.
|
||||
#block_banned_ips = true
|
||||
|
||||
@ -134,18 +120,15 @@ command = "java -Xmx1G -Xms1G -jar server.jar --nogui"
|
||||
# The client will be teleported to the real server once it is ready.
|
||||
# This may keep the client occupied forever if no timeout is set.
|
||||
# Consumes client, not allowing other join methods afterwards.
|
||||
# See: https://git.io/JMIi4
|
||||
|
||||
# !!! WARNING !!!
|
||||
# This is highly experimental, incomplete and unstable.
|
||||
# This may break the game and crash clients.
|
||||
# Don't enable this unless you know what you're doing.
|
||||
#
|
||||
# - Server must be in offline mode
|
||||
# - Server must use Minecraft version 1.16.3 to 1.17.1 (tested with 1.17.1)
|
||||
# - Server must use vanilla Minecraft
|
||||
# - May work with Forge, enable in config, depends on used mods, test before use
|
||||
# - Does not work with other mods, such as FTB
|
||||
# - Only works with offline mode
|
||||
# - Only works with vanilla Minecraft clients, does not work with modded
|
||||
# - Only tested with Minecraft 1.17.1
|
||||
|
||||
# Maximum time in seconds in the lobby while the server starts.
|
||||
#timeout = 600
|
||||
@ -187,4 +170,4 @@ command = "java -Xmx1G -Xms1G -jar server.jar --nogui"
|
||||
[config]
|
||||
# lazymc version this configuration is for.
|
||||
# Don't change unless you know what you're doing.
|
||||
version = "0.2.10"
|
||||
version = "0.2.4"
|
||||
|
@ -1,7 +1,5 @@
|
||||
#!/bin/bash
|
||||
|
||||
# See: https://git.io/JMIKH
|
||||
|
||||
# Server JAR file, set this to your own
|
||||
FILE=server.jar
|
||||
|
||||
|
@ -9,7 +9,7 @@ use crate::util::error::{quit, quit_error, ErrorHintsBuilder};
|
||||
/// Invoke config test command.
|
||||
pub fn invoke(matches: &ArgMatches) {
|
||||
// Get config path, attempt to canonicalize
|
||||
let mut path = PathBuf::from(matches.get_one::<String>("config").unwrap());
|
||||
let mut path = PathBuf::from(matches.value_of("config").unwrap());
|
||||
if let Ok(p) = path.canonicalize() {
|
||||
path = p;
|
||||
}
|
||||
|
@ -8,7 +8,7 @@ use crate::util::error::{quit_error, quit_error_msg, ErrorHintsBuilder};
|
||||
/// Invoke config test command.
|
||||
pub fn invoke(matches: &ArgMatches) {
|
||||
// Get config path, attempt to canonicalize
|
||||
let mut path = PathBuf::from(matches.get_one::<String>("config").unwrap());
|
||||
let mut path = PathBuf::from(matches.value_of("config").unwrap());
|
||||
if let Ok(p) = path.canonicalize() {
|
||||
path = p;
|
||||
}
|
||||
|
25
src/cli.rs
25
src/cli.rs
@ -1,28 +1,23 @@
|
||||
use clap::{Arg, Command};
|
||||
use clap::{App, AppSettings, Arg};
|
||||
|
||||
/// The clap app for CLI argument parsing.
|
||||
pub fn app() -> Command {
|
||||
Command::new(crate_name!())
|
||||
pub fn app() -> App<'static> {
|
||||
App::new(crate_name!())
|
||||
.version(crate_version!())
|
||||
.author(crate_authors!())
|
||||
.about(crate_description!())
|
||||
.subcommand(
|
||||
Command::new("start")
|
||||
App::new("start")
|
||||
.alias("run")
|
||||
.about("Start lazymc and server (default)"),
|
||||
)
|
||||
.subcommand(
|
||||
Command::new("config")
|
||||
App::new("config")
|
||||
.alias("cfg")
|
||||
.about("Config actions")
|
||||
.arg_required_else_help(true)
|
||||
.subcommand_required(true)
|
||||
.subcommand(
|
||||
Command::new("generate")
|
||||
.alias("gen")
|
||||
.about("Generate config"),
|
||||
)
|
||||
.subcommand(Command::new("test").about("Test config")),
|
||||
.setting(AppSettings::SubcommandRequiredElseHelp)
|
||||
.subcommand(App::new("generate").alias("gen").about("Generate config"))
|
||||
.subcommand(App::new("test").about("Test config")),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("config")
|
||||
@ -32,7 +27,7 @@ pub fn app() -> Command {
|
||||
.global(true)
|
||||
.value_name("FILE")
|
||||
.default_value(crate::config::CONFIG_FILE)
|
||||
.help("Use config file")
|
||||
.num_args(1),
|
||||
.about("Use config file")
|
||||
.takes_value(true),
|
||||
)
|
||||
}
|
||||
|
@ -15,14 +15,14 @@ use crate::util::serde::to_socket_addrs;
|
||||
pub const CONFIG_FILE: &str = "lazymc.toml";
|
||||
|
||||
/// Configuration version user should be using, or warning will be shown.
|
||||
const CONFIG_VERSION: &str = "0.2.8";
|
||||
const CONFIG_VERSION: &str = "0.2.1";
|
||||
|
||||
/// Load config from file, based on CLI arguments.
|
||||
///
|
||||
/// Quits with an error message on failure.
|
||||
pub fn load(matches: &ArgMatches) -> Config {
|
||||
// Get config path, attempt to canonicalize
|
||||
let mut path = PathBuf::from(matches.get_one::<String>("config").unwrap());
|
||||
let mut path = PathBuf::from(matches.value_of("config").unwrap());
|
||||
if let Ok(p) = path.canonicalize() {
|
||||
path = p;
|
||||
}
|
||||
@ -174,11 +174,6 @@ pub struct Server {
|
||||
)]
|
||||
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.
|
||||
#[serde(default)]
|
||||
pub wake_on_start: bool,
|
||||
@ -187,14 +182,6 @@ pub struct Server {
|
||||
#[serde(default)]
|
||||
pub wake_on_crash: bool,
|
||||
|
||||
/// Probe required server details when starting lazymc, wakes server on start.
|
||||
#[serde(default)]
|
||||
pub probe_on_start: bool,
|
||||
|
||||
/// Whether this server runs forge.
|
||||
#[serde(default)]
|
||||
pub forge: bool,
|
||||
|
||||
/// Server starting timeout. Force kill server process if it takes longer.
|
||||
#[serde(default = "u32_300")]
|
||||
pub start_timeout: u32,
|
||||
@ -203,10 +190,6 @@ pub struct Server {
|
||||
#[serde(default = "u32_150")]
|
||||
pub stop_timeout: u32,
|
||||
|
||||
/// To wake server, user must be in server whitelist if enabled on server.
|
||||
#[serde(default = "bool_true")]
|
||||
pub wake_whitelist: bool,
|
||||
|
||||
/// Block banned IPs as listed in banned-ips.json in server directory.
|
||||
#[serde(default = "bool_true")]
|
||||
pub block_banned_ips: bool,
|
||||
@ -483,13 +466,19 @@ impl Default for Advanced {
|
||||
}
|
||||
|
||||
/// Config configuration.
|
||||
#[derive(Debug, Deserialize, Default)]
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(default)]
|
||||
pub struct ConfigConfig {
|
||||
/// Configuration for lazymc version.
|
||||
pub version: Option<String>,
|
||||
}
|
||||
|
||||
impl Default for ConfigConfig {
|
||||
fn default() -> Self {
|
||||
Self { version: None }
|
||||
}
|
||||
}
|
||||
|
||||
fn option_pathbuf_dot() -> Option<PathBuf> {
|
||||
Some(".".into())
|
||||
}
|
||||
|
255
src/forge.rs
255
src/forge.rs
@ -1,255 +0,0 @@
|
||||
#[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(())
|
||||
}
|
@ -21,12 +21,6 @@ pub async fn occupy(
|
||||
) -> Result<MethodResult, ()> {
|
||||
trace!(target: "lazymc", "Using lobby method to occupy joining client");
|
||||
|
||||
// Must be ready to lobby
|
||||
if must_still_probe(&config, &server).await {
|
||||
warn!(target: "lazymc", "Client connected but lobby is not ready, using next join method, probing not completed");
|
||||
return Ok(MethodResult::Continue(inbound));
|
||||
}
|
||||
|
||||
// Start lobby
|
||||
lobby::serve(client, client_info, inbound, config, server, inbound_queue).await?;
|
||||
|
||||
@ -34,13 +28,3 @@ pub async fn occupy(
|
||||
|
||||
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
|
||||
}
|
||||
|
434
src/lobby.rs
434
src/lobby.rs
@ -1,14 +1,20 @@
|
||||
use std::io::ErrorKind;
|
||||
use std::ops::Deref;
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use bytes::BytesMut;
|
||||
use futures::FutureExt;
|
||||
use minecraft_protocol::data::chat::{Message, Payload};
|
||||
use minecraft_protocol::decoder::Decoder;
|
||||
use minecraft_protocol::version::v1_14_4::login::{
|
||||
LoginPluginRequest, LoginPluginResponse, LoginStart, LoginSuccess, SetCompression,
|
||||
use minecraft_protocol::version::v1_14_4::handshake::Handshake;
|
||||
use minecraft_protocol::version::v1_14_4::login::{LoginStart, LoginSuccess, SetCompression};
|
||||
use minecraft_protocol::version::v1_17_1::game::{
|
||||
ClientBoundKeepAlive, ClientBoundPluginMessage, JoinGame, NamedSoundEffect,
|
||||
PlayerPositionAndLook, Respawn, SetTitleSubtitle, SetTitleText, SetTitleTimes, TimeUpdate,
|
||||
};
|
||||
use nbt::CompoundTag;
|
||||
use tokio::io::AsyncWriteExt;
|
||||
use tokio::net::tcp::{ReadHalf, WriteHalf};
|
||||
use tokio::net::TcpStream;
|
||||
@ -16,18 +22,19 @@ use tokio::select;
|
||||
use tokio::time;
|
||||
|
||||
use crate::config::*;
|
||||
use crate::forge;
|
||||
use crate::mc::uuid;
|
||||
use crate::mc::{self, uuid};
|
||||
use crate::net;
|
||||
use crate::proto;
|
||||
use crate::proto::client::{Client, ClientInfo, ClientState};
|
||||
use crate::proto::packets::play::join_game::JoinGameData;
|
||||
use crate::proto::{packet, packets};
|
||||
use crate::proxy;
|
||||
use crate::server::{Server, State};
|
||||
|
||||
/// Interval to send keep-alive packets at.
|
||||
pub const KEEP_ALIVE_INTERVAL: Duration = Duration::from_secs(10);
|
||||
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.
|
||||
const SERVER_CONNECT_TIMEOUT: Duration = Duration::from_secs(2 * 60);
|
||||
@ -42,9 +49,14 @@ 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
|
||||
/// 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);
|
||||
|
||||
/// 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.
|
||||
///
|
||||
/// The client must be in the login state, or this will error.
|
||||
@ -97,14 +109,6 @@ pub async fn serve(
|
||||
|
||||
debug!(target: "lazymc::lobby", "Login on lobby server (user: {})", login_start.name);
|
||||
|
||||
// Replay Forge payload
|
||||
if config.server.forge {
|
||||
forge::replay_login_payload(client, &mut inbound, server.clone(), &mut inbound_buf)
|
||||
.await?;
|
||||
let (_returned_reader, returned_writer) = inbound.split();
|
||||
writer = returned_writer;
|
||||
}
|
||||
|
||||
// Respond with set compression if compression is enabled based on threshold
|
||||
if proto::COMPRESSION_THRESHOLD >= 0 {
|
||||
trace!(target: "lazymc::lobby", "Enabling compression for lobby client because server has it enabled (threshold: {})", proto::COMPRESSION_THRESHOLD);
|
||||
@ -119,33 +123,25 @@ pub async fn serve(
|
||||
trace!(target: "lazymc::lobby", "Client login success, sending required play packets for lobby world");
|
||||
|
||||
// Send packets to client required to get into workable play state for lobby world
|
||||
send_lobby_play_packets(client, &client_info, &mut writer, &server).await?;
|
||||
send_lobby_play_packets(client, &mut writer, &server).await?;
|
||||
|
||||
// Wait for server to come online
|
||||
stage_wait(client, &client_info, &server, &config, &mut writer).await?;
|
||||
|
||||
// Start new connection to server
|
||||
let server_client_info = client_info.clone();
|
||||
// Wait for server to come online, then set up new connection to it
|
||||
stage_wait(client, &server, &config, &mut writer).await?;
|
||||
let (server_client, mut outbound, mut server_buf) =
|
||||
connect_to_server(&server_client_info, &inbound, &config).await?;
|
||||
connect_to_server(client_info, &inbound, &config).await?;
|
||||
let (returned_reader, returned_writer) = inbound.split();
|
||||
reader = returned_reader;
|
||||
writer = returned_writer;
|
||||
|
||||
// Grab join game packet from server
|
||||
let join_game_data = wait_for_server_join_game(
|
||||
&server_client,
|
||||
&server_client_info,
|
||||
&mut outbound,
|
||||
&mut server_buf,
|
||||
)
|
||||
.await?;
|
||||
let join_game =
|
||||
wait_for_server_join_game(&server_client, &mut outbound, &mut server_buf).await?;
|
||||
|
||||
// Reset lobby title
|
||||
packets::play::title::send(client, &client_info, &mut writer, "").await?;
|
||||
send_lobby_title(client, &mut writer, "").await?;
|
||||
|
||||
// Play ready sound if configured
|
||||
play_lobby_ready_sound(client, &client_info, &mut writer, &config).await?;
|
||||
play_lobby_ready_sound(client, &mut writer, &config).await?;
|
||||
|
||||
// Wait a second because Notchian servers are slow
|
||||
// See: https://wiki.vg/Protocol#Login_Success
|
||||
@ -153,8 +149,7 @@ pub async fn serve(
|
||||
time::sleep(SERVER_WARMUP).await;
|
||||
|
||||
// Send respawn packet, initiates teleport to real server world
|
||||
packets::play::respawn::lobby_send(client, &client_info, &mut writer, join_game_data)
|
||||
.await?;
|
||||
send_respawn_from_join(client, &mut writer, join_game).await?;
|
||||
|
||||
// Drain inbound connection so we don't confuse the server
|
||||
// TODO: can we void everything? we might need to forward everything to server except
|
||||
@ -169,8 +164,11 @@ pub async fn serve(
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// TODO: when receiving Login Plugin Request, respond with empty payload
|
||||
// See: https://wiki.vg/Protocol#Login_Plugin_Request
|
||||
|
||||
// Show unhandled packet warning
|
||||
debug!(target: "lazymc", "Got unhandled packet:");
|
||||
debug!(target: "lazymc", "Received unhandled packet:");
|
||||
debug!(target: "lazymc", "- State: {:?}", client_state);
|
||||
debug!(target: "lazymc", "- Packet ID: 0x{:02X} ({})", packet.id, packet.id);
|
||||
}
|
||||
@ -211,7 +209,6 @@ async fn respond_login_success(
|
||||
/// Play lobby ready sound effect if configured.
|
||||
async fn play_lobby_ready_sound(
|
||||
client: &Client,
|
||||
client_info: &ClientInfo,
|
||||
writer: &mut WriteHalf<'_>,
|
||||
config: &Config,
|
||||
) -> Result<(), ()> {
|
||||
@ -223,8 +220,8 @@ async fn play_lobby_ready_sound(
|
||||
}
|
||||
|
||||
// Play sound effect
|
||||
packets::play::player_pos::send(client, client_info, writer).await?;
|
||||
packets::play::sound::send(client, client_info, writer, sound_name).await?;
|
||||
send_lobby_player_pos(client, writer).await?;
|
||||
send_lobby_sound_effect(client, writer, sound_name).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@ -233,33 +230,245 @@ async fn play_lobby_ready_sound(
|
||||
/// Send packets to client to get workable play state for lobby world.
|
||||
async fn send_lobby_play_packets(
|
||||
client: &Client,
|
||||
client_info: &ClientInfo,
|
||||
writer: &mut WriteHalf<'_>,
|
||||
server: &Server,
|
||||
) -> Result<(), ()> {
|
||||
// See: https://wiki.vg/Protocol_FAQ#What.27s_the_normal_login_sequence_for_a_client.3F
|
||||
|
||||
// Send initial game join
|
||||
packets::play::join_game::lobby_send(client, client_info, writer, server).await?;
|
||||
send_lobby_join_game(client, writer, server).await?;
|
||||
|
||||
// Send server brand
|
||||
packets::play::server_brand::send(client, client_info, writer).await?;
|
||||
send_lobby_brand(client, writer).await?;
|
||||
|
||||
// Send spawn and player position, disables 'download terrain' screen
|
||||
packets::play::player_pos::send(client, client_info, writer).await?;
|
||||
send_lobby_player_pos(client, writer).await?;
|
||||
|
||||
// Notify client of world time, required once before keep-alive packets
|
||||
packets::play::time_update::send(client, client_info, writer).await?;
|
||||
send_lobby_time_update(client, writer).await?;
|
||||
|
||||
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.
|
||||
///
|
||||
/// This will keep sending keep-alive and title packets to the client until it is dropped.
|
||||
async fn keep_alive_loop(
|
||||
client: &Client,
|
||||
client_info: &ClientInfo,
|
||||
writer: &mut WriteHalf<'_>,
|
||||
config: &Config,
|
||||
) -> Result<(), ()> {
|
||||
@ -271,10 +480,8 @@ async fn keep_alive_loop(
|
||||
trace!(target: "lazymc::lobby", "Sending keep-alive sequence to lobby client");
|
||||
|
||||
// Send keep alive and title packets
|
||||
packets::play::keep_alive::send(client, client_info, writer).await?;
|
||||
packets::play::title::send(client, client_info, writer, &config.join.lobby.message).await?;
|
||||
|
||||
// TODO: verify we receive correct keep alive response
|
||||
send_keep_alive(client, writer).await?;
|
||||
send_lobby_title(client, writer, &config.join.lobby.message).await?;
|
||||
}
|
||||
}
|
||||
|
||||
@ -285,13 +492,12 @@ async fn keep_alive_loop(
|
||||
/// During this stage we keep sending keep-alive and title packets to the client to keep it active.
|
||||
async fn stage_wait(
|
||||
client: &Client,
|
||||
client_info: &ClientInfo,
|
||||
server: &Server,
|
||||
config: &Config,
|
||||
writer: &mut WriteHalf<'_>,
|
||||
) -> Result<(), ()> {
|
||||
select! {
|
||||
a = keep_alive_loop(client, client_info, writer, config) => a,
|
||||
a = keep_alive_loop(client, writer, config) => a,
|
||||
b = wait_for_server(server, config) => b,
|
||||
}
|
||||
}
|
||||
@ -355,7 +561,7 @@ async fn wait_for_server(server: &Server, config: &Config) -> Result<(), ()> {
|
||||
///
|
||||
/// This will initialize the connection to the play state. Client details are used.
|
||||
async fn connect_to_server(
|
||||
client_info: &ClientInfo,
|
||||
client_info: ClientInfo,
|
||||
inbound: &TcpStream,
|
||||
config: &Config,
|
||||
) -> Result<(Client, TcpStream, BytesMut), ()> {
|
||||
@ -374,7 +580,7 @@ async fn connect_to_server(
|
||||
/// This will initialize the connection to the play state. Client details are used.
|
||||
// TODO: clean this up
|
||||
async fn connect_to_server_no_timeout(
|
||||
client_info: &ClientInfo,
|
||||
client_info: ClientInfo,
|
||||
inbound: &TcpStream,
|
||||
config: &Config,
|
||||
) -> Result<(Client, TcpStream, BytesMut), ()> {
|
||||
@ -402,14 +608,14 @@ async fn connect_to_server_no_timeout(
|
||||
|
||||
let (mut reader, mut writer) = outbound.split();
|
||||
|
||||
// Replay client handshake packet
|
||||
assert_eq!(
|
||||
client_info.handshake.as_ref().unwrap().next_state,
|
||||
ClientState::Login.to_id(),
|
||||
"Client handshake should have login as next state"
|
||||
);
|
||||
// Handshake packet
|
||||
packet::write_packet(
|
||||
client_info.handshake.clone().unwrap(),
|
||||
Handshake {
|
||||
protocol_version: client_info.protocol_version.unwrap(),
|
||||
server_addr: config.server.address.ip().to_string(),
|
||||
server_port: config.server.address.port(),
|
||||
next_state: ClientState::Login.to_id(),
|
||||
},
|
||||
&tmp_client,
|
||||
&mut writer,
|
||||
)
|
||||
@ -418,7 +624,7 @@ async fn connect_to_server_no_timeout(
|
||||
// Request login start
|
||||
packet::write_packet(
|
||||
LoginStart {
|
||||
name: client_info.username.clone().ok_or(())?,
|
||||
name: client_info.username.ok_or(())?,
|
||||
},
|
||||
&tmp_client,
|
||||
&mut writer,
|
||||
@ -447,7 +653,9 @@ async fn connect_to_server_no_timeout(
|
||||
{
|
||||
// Decode compression packet
|
||||
let set_compression =
|
||||
SetCompression::decode(&mut packet.data.as_slice()).map_err(|_| ())?;
|
||||
SetCompression::decode(&mut packet.data.as_slice()).map_err(|err| {
|
||||
dbg!(err);
|
||||
})?;
|
||||
|
||||
// Client and server compression threshold should match, show warning if not
|
||||
if set_compression.threshold != proto::COMPRESSION_THRESHOLD {
|
||||
@ -464,59 +672,9 @@ async fn connect_to_server_no_timeout(
|
||||
continue;
|
||||
}
|
||||
|
||||
// Catch encryption requests
|
||||
if client_state == ClientState::Login
|
||||
&& packet.id == packets::login::CLIENT_ENCRYPTION_REQUEST
|
||||
{
|
||||
error!(
|
||||
target: "lazymc::lobby",
|
||||
"Got encryption request from server, this is unsupported. Server must be in offline mode to use lobby.",
|
||||
);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
// Hijack login plugin request
|
||||
if client_state == ClientState::Login
|
||||
&& packet.id == packets::login::CLIENT_LOGIN_PLUGIN_REQUEST
|
||||
{
|
||||
// Decode login plugin request
|
||||
let plugin_request =
|
||||
LoginPluginRequest::decode(&mut packet.data.as_slice()).map_err(|err| {
|
||||
dbg!(err);
|
||||
})?;
|
||||
|
||||
// Respond with Forge messages
|
||||
if config.server.forge {
|
||||
trace!(target: "lazymc::lobby", "Got login plugin request from server, responding with Forge reply");
|
||||
|
||||
// Respond to Forge login plugin request
|
||||
forge::respond_login_plugin_request(&tmp_client, plugin_request, &mut writer)
|
||||
.await?;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
warn!(target: "lazymc::lobby", "Got unexpected login plugin request from server, you may need to enable Forge support");
|
||||
|
||||
// Write unsuccesful login plugin response
|
||||
packet::write_packet(
|
||||
LoginPluginResponse {
|
||||
message_id: plugin_request.message_id,
|
||||
successful: false,
|
||||
data: vec![],
|
||||
},
|
||||
&tmp_client,
|
||||
&mut writer,
|
||||
)
|
||||
.await?;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// Hijack login success
|
||||
if client_state == ClientState::Login && packet.id == packets::login::CLIENT_LOGIN_SUCCESS {
|
||||
trace!(target: "lazymc::lobby", "Got login success from server connection, change to play mode");
|
||||
trace!(target: "lazymc::lobby", "Received login success from server connection, change to play mode");
|
||||
|
||||
// TODO: parse this packet to ensure it's fine
|
||||
// let login_success =
|
||||
@ -536,23 +694,8 @@ async fn connect_to_server_no_timeout(
|
||||
return Ok((tmp_client, outbound, buf));
|
||||
}
|
||||
|
||||
// Hijack disconnect
|
||||
if client_state == ClientState::Login && packet.id == packets::login::CLIENT_DISCONNECT {
|
||||
error!(target: "lazymc::lobby", "Got disconnect from server connection");
|
||||
|
||||
// // Decode disconnect packet
|
||||
// let login_disconnect =
|
||||
// LoginDisconnect::decode(&mut packet.data.as_slice()).map_err(|err| {
|
||||
// dbg!(err);
|
||||
// })?;
|
||||
|
||||
// TODO: report/forward error to client
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
// Show unhandled packet warning
|
||||
debug!(target: "lazymc::lobby", "Got unhandled packet from server in connect_to_server:");
|
||||
debug!(target: "lazymc::lobby", "Received unhandled packet from server in connect_to_server:");
|
||||
debug!(target: "lazymc::lobby", "- State: {:?}", client_state);
|
||||
debug!(target: "lazymc::lobby", "- Packet ID: 0x{:02X} ({})", packet.id, packet.id);
|
||||
}
|
||||
@ -568,13 +711,12 @@ async fn connect_to_server_no_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, ()> {
|
||||
) -> Result<JoinGame, ()> {
|
||||
time::timeout(
|
||||
SERVER_JOIN_GAME_TIMEOUT,
|
||||
wait_for_server_join_game_no_timeout(client, client_info, outbound, buf),
|
||||
wait_for_server_join_game_no_timeout(client, outbound, buf),
|
||||
)
|
||||
.await
|
||||
.map_err(|_| {
|
||||
@ -589,10 +731,9 @@ async fn wait_for_server_join_game(
|
||||
// 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, ()> {
|
||||
) -> Result<JoinGame, ()> {
|
||||
let (mut reader, mut _writer) = outbound.split();
|
||||
|
||||
loop {
|
||||
@ -607,17 +748,16 @@ async fn wait_for_server_join_game_no_timeout(
|
||||
};
|
||||
|
||||
// Catch join game
|
||||
if packets::play::join_game::is_packet(client_info, packet.id) {
|
||||
// Parse join game data
|
||||
let join_game_data = JoinGameData::from_packet(client_info, packet).map_err(|err| {
|
||||
warn!(target: "lazymc::lobby", "Failed to parse join game packet: {:?}", err);
|
||||
if packet.id == packets::play::CLIENT_JOIN_GAME {
|
||||
let join_game = JoinGame::decode(&mut packet.data.as_slice()).map_err(|err| {
|
||||
dbg!(err);
|
||||
})?;
|
||||
|
||||
return Ok(join_game_data);
|
||||
return Ok(join_game);
|
||||
}
|
||||
|
||||
// Show unhandled packet warning
|
||||
debug!(target: "lazymc::lobby", "Got unhandled packet from server in wait_for_server_join_game:");
|
||||
debug!(target: "lazymc::lobby", "Received unhandled packet from server in wait_for_server_join_game:");
|
||||
debug!(target: "lazymc::lobby", "- Packet ID: 0x{:02X} ({})", packet.id, packet.id);
|
||||
}
|
||||
|
||||
@ -662,3 +802,21 @@ 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()
|
||||
}
|
||||
|
10
src/main.rs
10
src/main.rs
@ -10,7 +10,6 @@ extern crate log;
|
||||
pub(crate) mod action;
|
||||
pub(crate) mod cli;
|
||||
pub(crate) mod config;
|
||||
pub(crate) mod forge;
|
||||
pub(crate) mod join;
|
||||
#[cfg(feature = "lobby")]
|
||||
pub(crate) mod lobby;
|
||||
@ -18,7 +17,6 @@ pub(crate) mod mc;
|
||||
pub(crate) mod monitor;
|
||||
pub(crate) mod net;
|
||||
pub(crate) mod os;
|
||||
pub(crate) mod probe;
|
||||
pub(crate) mod proto;
|
||||
pub(crate) mod proxy;
|
||||
pub(crate) mod server;
|
||||
@ -29,11 +27,7 @@ pub(crate) mod util;
|
||||
|
||||
use std::env;
|
||||
|
||||
use clap::Command;
|
||||
|
||||
// Compile time feature compatability check.
|
||||
#[cfg(all(windows, not(feature = "rcon")))]
|
||||
compile_error!("Must enable \"rcon\" feature on Windows.");
|
||||
use clap::App;
|
||||
|
||||
/// Default log level if none is set.
|
||||
const LOG_DEFAULT: &str = "info";
|
||||
@ -63,7 +57,7 @@ fn init_log() {
|
||||
}
|
||||
|
||||
/// Invoke an action.
|
||||
fn invoke_action(app: Command) -> Result<(), ()> {
|
||||
fn invoke_action(app: App) -> Result<(), ()> {
|
||||
let matches = app.get_matches();
|
||||
|
||||
// Config operations
|
||||
|
@ -1,112 +0,0 @@
|
||||
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()
|
||||
}
|
@ -1,9 +1,7 @@
|
||||
use base64::Engine;
|
||||
|
||||
use crate::proto::client::ClientInfo;
|
||||
|
||||
/// Protocol version since when favicons are supported.
|
||||
const FAVICON_PROTOCOL_VERSION: u32 = 4;
|
||||
const FAVICON_PROTOCOL_VERSION: i32 = 4;
|
||||
|
||||
/// Get default server status favicon.
|
||||
pub fn default_favicon() -> String {
|
||||
@ -14,11 +12,7 @@ pub fn default_favicon() -> String {
|
||||
///
|
||||
/// 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)
|
||||
)
|
||||
format!("{}{}", "data:image/png;base64,", base64::encode(data))
|
||||
}
|
||||
|
||||
/// Check whether the status response favicon is supported based on the given client info.
|
||||
@ -26,7 +20,7 @@ pub fn encode_favicon(data: &[u8]) -> String {
|
||||
/// Defaults to `true` if unsure.
|
||||
pub fn supports_favicon(client_info: &ClientInfo) -> bool {
|
||||
client_info
|
||||
.protocol
|
||||
.protocol_version
|
||||
.map(|p| p >= FAVICON_PROTOCOL_VERSION)
|
||||
.unwrap_or(true)
|
||||
}
|
||||
|
@ -1,13 +1,10 @@
|
||||
pub mod ban;
|
||||
#[cfg(feature = "lobby")]
|
||||
pub mod dimension;
|
||||
pub mod favicon;
|
||||
#[cfg(feature = "rcon")]
|
||||
pub mod rcon;
|
||||
pub mod server_properties;
|
||||
#[cfg(feature = "lobby")]
|
||||
pub mod uuid;
|
||||
pub mod whitelist;
|
||||
|
||||
/// Minecraft ticks per second.
|
||||
#[allow(unused)]
|
||||
|
@ -114,7 +114,7 @@ fn rewrite_contents(contents: String, mut changes: HashMap<&str, String>) -> Opt
|
||||
}
|
||||
|
||||
// Try to split property
|
||||
let (key, value) = match line.split_once('=') {
|
||||
let (key, value) = match line.split_once("=") {
|
||||
Some(result) => result,
|
||||
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
|
||||
if let Some((_, new)) = changes.remove_entry(key.trim().to_lowercase().as_str()) {
|
||||
if value != new {
|
||||
line = format!("{key}={new}");
|
||||
line = format!("{}={}", key, new);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
@ -134,7 +134,7 @@ fn rewrite_contents(contents: String, mut changes: HashMap<&str, String>) -> Opt
|
||||
|
||||
// Append any missed changes
|
||||
for (key, value) in changes {
|
||||
new_contents += &format!("{EOL}{key}={value}");
|
||||
new_contents += &format!("{}{}={}", EOL, key, value);
|
||||
changed = true;
|
||||
}
|
||||
|
||||
@ -145,37 +145,3 @@ fn rewrite_contents(contents: String, mut changes: HashMap<&str, String>) -> Opt
|
||||
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())
|
||||
}
|
||||
|
@ -5,20 +5,20 @@ use uuid::Uuid;
|
||||
const OFFLINE_PLAYER_NAMESPACE: &str = "OfflinePlayer:";
|
||||
|
||||
/// Get UUID for given player username.
|
||||
fn player_uuid(username: &str) -> Uuid {
|
||||
pub 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}"))
|
||||
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>
|
||||
/// Ported from: https://git.io/J1b6A
|
||||
fn java_name_uuid_from_bytes(data: &[u8]) -> Uuid {
|
||||
let mut hasher = Md5::new();
|
||||
hasher.update(data);
|
||||
|
@ -1,107 +0,0 @@
|
||||
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())
|
||||
}
|
@ -57,13 +57,13 @@ pub async fn monitor_server(config: Arc<Config>, server: Arc<Server>) {
|
||||
|
||||
// Sleep server when it's bedtime
|
||||
if server.should_sleep(&config).await {
|
||||
info!(target: "lazymc::monitor", "Server has been idle, sleeping...");
|
||||
info!(target: "lazymc::montior", "Server has been idle, sleeping...");
|
||||
server.stop(&config).await;
|
||||
}
|
||||
|
||||
// Check whether we should force kill server
|
||||
if server.should_kill().await {
|
||||
error!(target: "lazymc::monitor", "Force killing server, took too long to start or stop");
|
||||
error!(target: "lazymc::montior", "Force killing server, took too long to start or stop");
|
||||
if !server.force_kill().await {
|
||||
warn!(target: "lazymc", "Failed to force kill server");
|
||||
}
|
||||
|
@ -1,16 +1,17 @@
|
||||
#[cfg(unix)]
|
||||
pub mod unix;
|
||||
#[cfg(windows)]
|
||||
pub mod windows;
|
||||
|
||||
#[cfg(unix)]
|
||||
use nix::{sys::signal, unistd::Pid};
|
||||
|
||||
/// Force kill process.
|
||||
///
|
||||
/// Results in undefined behavior if PID is invalid.
|
||||
#[allow(unreachable_code)]
|
||||
pub fn force_kill(pid: u32) -> bool {
|
||||
#[cfg(unix)]
|
||||
return unix_signal(pid, signal::SIGKILL);
|
||||
unsafe {
|
||||
return unix::force_kill(pid);
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
unsafe {
|
||||
@ -21,57 +22,20 @@ pub fn force_kill(pid: u32) -> bool {
|
||||
}
|
||||
|
||||
/// Gracefully kill process.
|
||||
///
|
||||
/// Results in undefined behavior if PID is invalid.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics on platforms other than Unix.
|
||||
#[allow(unreachable_code, dead_code, unused_variables)]
|
||||
pub fn kill_gracefully(pid: u32) -> bool {
|
||||
#[cfg(unix)]
|
||||
return unix_signal(pid, signal::SIGTERM);
|
||||
unsafe {
|
||||
return unix::kill_gracefully(pid);
|
||||
}
|
||||
|
||||
unimplemented!(
|
||||
"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
|
||||
}
|
||||
};
|
||||
}
|
||||
|
27
src/os/unix.rs
Normal file
27
src/os/unix.rs
Normal file
@ -0,0 +1,27 @@
|
||||
/// 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
362
src/probe.rs
@ -1,362 +0,0 @@
|
||||
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(())
|
||||
}
|
@ -2,8 +2,6 @@ use std::net::SocketAddr;
|
||||
use std::sync::atomic::{AtomicI32, Ordering};
|
||||
use std::sync::Mutex;
|
||||
|
||||
use minecraft_protocol::version::v1_14_4::handshake::Handshake;
|
||||
|
||||
/// Client state.
|
||||
///
|
||||
/// Note: this does not keep track of encryption states.
|
||||
@ -115,11 +113,8 @@ impl Default for ClientState {
|
||||
/// Client info, useful during connection handling.
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct ClientInfo {
|
||||
/// Used protocol version.
|
||||
pub protocol: Option<u32>,
|
||||
|
||||
/// Handshake as received from client.
|
||||
pub handshake: Option<Handshake>,
|
||||
/// Client protocol version.
|
||||
pub protocol_version: Option<i32>,
|
||||
|
||||
/// Client username.
|
||||
pub username: Option<String>,
|
||||
@ -129,10 +124,4 @@ impl ClientInfo {
|
||||
pub fn empty() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/// Get protocol version.
|
||||
pub fn protocol(&self) -> Option<u32> {
|
||||
self.protocol
|
||||
.or_else(|| self.handshake.as_ref().map(|h| h.protocol_version as u32))
|
||||
}
|
||||
}
|
||||
|
@ -9,7 +9,7 @@ pub mod packets;
|
||||
/// in the configuration.
|
||||
///
|
||||
/// Should be kept up-to-date with latest supported Minecraft version by lazymc.
|
||||
pub const PROTO_DEFAULT_VERSION: &str = "1.19.3";
|
||||
pub const PROTO_DEFAULT_VERSION: &str = "1.17.1";
|
||||
|
||||
/// Default minecraft protocol version.
|
||||
///
|
||||
@ -17,7 +17,7 @@ pub const PROTO_DEFAULT_VERSION: &str = "1.19.3";
|
||||
/// in the configuration.
|
||||
///
|
||||
/// Should be kept up-to-date with latest supported Minecraft version by lazymc.
|
||||
pub const PROTO_DEFAULT_PROTOCOL: u32 = 761;
|
||||
pub const PROTO_DEFAULT_PROTOCOL: u32 = 756;
|
||||
|
||||
/// Compression threshold to use.
|
||||
// TODO: read this from server.properties instead
|
||||
|
@ -1,4 +1,3 @@
|
||||
use std::fmt::Debug;
|
||||
use std::io::prelude::*;
|
||||
|
||||
use bytes::BytesMut;
|
||||
@ -45,22 +44,11 @@ impl RawPacket {
|
||||
///
|
||||
/// This decodes both compressed and uncompressed packets based on the client threshold
|
||||
/// preference.
|
||||
pub fn decode_with_len(client: &Client, mut buf: &[u8]) -> Result<Self, ()> {
|
||||
pub fn decode(client: &Client, mut buf: &[u8]) -> Result<Self, ()> {
|
||||
// Read length
|
||||
let (read, len) = types::read_var_int(buf)?;
|
||||
buf = &buf[read..][..len as usize];
|
||||
|
||||
// TODO: assert buffer length!
|
||||
|
||||
Self::decode_without_len(client, buf)
|
||||
}
|
||||
|
||||
/// Decode packet from raw buffer without packet length.
|
||||
///
|
||||
/// This decodes both compressed and uncompressed packets based on the client threshold
|
||||
/// preference.
|
||||
/// The length is given, and not included in the buffer itself.
|
||||
pub fn decode_without_len(client: &Client, mut buf: &[u8]) -> Result<Self, ()> {
|
||||
// If no compression is used, read remaining packet ID and data
|
||||
if !client.is_compressed() {
|
||||
// Read packet ID and data
|
||||
@ -97,20 +85,7 @@ impl RawPacket {
|
||||
/// Encode packet to raw buffer.
|
||||
///
|
||||
/// This compresses packets based on the client threshold preference.
|
||||
pub fn encode_with_len(&self, client: &Client) -> Result<Vec<u8>, ()> {
|
||||
// Encode packet without length
|
||||
let mut payload = self.encode_without_len(client)?;
|
||||
|
||||
// Add length header
|
||||
let mut packet = types::encode_var_int(payload.len() as i32)?;
|
||||
packet.append(&mut payload);
|
||||
Ok(packet)
|
||||
}
|
||||
|
||||
/// Encode packet to raw buffer without length header.
|
||||
///
|
||||
/// This compresses packets based on the client threshold preference.
|
||||
pub fn encode_without_len(&self, client: &Client) -> Result<Vec<u8>, ()> {
|
||||
pub fn encode(&self, client: &Client) -> Result<Vec<u8>, ()> {
|
||||
let threshold = client.compressed();
|
||||
if threshold >= 0 {
|
||||
self.encode_compressed(threshold)
|
||||
@ -128,7 +103,8 @@ impl RawPacket {
|
||||
// Determine whether to compress, encode data length bytes
|
||||
let data_len = payload.len() as i32;
|
||||
let compress = data_len > threshold;
|
||||
let data_len_header = if compress { data_len } else { 0 };
|
||||
let mut data_len_bytes =
|
||||
types::encode_var_int(if compress { data_len } else { 0 }).unwrap();
|
||||
|
||||
// Compress payload
|
||||
if compress {
|
||||
@ -141,8 +117,10 @@ impl RawPacket {
|
||||
})?;
|
||||
}
|
||||
|
||||
// Add data length header
|
||||
let mut packet = types::encode_var_int(data_len_header).unwrap();
|
||||
// Encapsulate payload with packet and data length
|
||||
let len = data_len_bytes.len() as i32 + payload.len() as i32;
|
||||
let mut packet = types::encode_var_int(len)?;
|
||||
packet.append(&mut data_len_bytes);
|
||||
packet.append(&mut payload);
|
||||
|
||||
Ok(packet)
|
||||
@ -150,8 +128,12 @@ impl RawPacket {
|
||||
|
||||
/// Encode uncompressed packet to raw buffer.
|
||||
fn encode_uncompressed(&self) -> Result<Vec<u8>, ()> {
|
||||
let mut packet = types::encode_var_int(self.id as i32)?;
|
||||
packet.extend_from_slice(&self.data);
|
||||
let mut data = types::encode_var_int(self.id as i32)?;
|
||||
data.extend_from_slice(&self.data);
|
||||
|
||||
let len = data.len() as i32;
|
||||
let mut packet = types::encode_var_int(len)?;
|
||||
packet.append(&mut data);
|
||||
|
||||
Ok(packet)
|
||||
}
|
||||
@ -212,23 +194,22 @@ pub async fn read_packet(
|
||||
}
|
||||
|
||||
// Parse packet, use full buffer since we'll read the packet length again
|
||||
// TODO: use decode_without_len, strip len from buffer
|
||||
let raw = buf.split_to(consumed + len as usize);
|
||||
let packet = RawPacket::decode_with_len(client, &raw)?;
|
||||
let packet = RawPacket::decode(client, &raw)?;
|
||||
|
||||
Ok(Some((packet, raw.to_vec())))
|
||||
}
|
||||
|
||||
/// Write packet to stream writer.
|
||||
pub async fn write_packet(
|
||||
packet: impl PacketId + Encoder + Debug,
|
||||
packet: impl PacketId + Encoder,
|
||||
client: &Client,
|
||||
writer: &mut WriteHalf<'_>,
|
||||
) -> Result<(), ()> {
|
||||
let mut data = Vec::new();
|
||||
packet.encode(&mut data).map_err(|_| ())?;
|
||||
|
||||
let response = RawPacket::new(packet.packet_id(), data).encode_with_len(client)?;
|
||||
let response = RawPacket::new(packet.packet_id(), data).encode(client)?;
|
||||
writer.write_all(&response).await.map_err(|_| ())?;
|
||||
|
||||
Ok(())
|
||||
|
41
src/proto/packets.rs
Normal file
41
src/proto/packets.rs
Normal file
@ -0,0 +1,41 @@
|
||||
//! Minecraft protocol packet IDs.
|
||||
|
||||
#![allow(unused)]
|
||||
|
||||
pub mod handshake {
|
||||
pub const SERVER_HANDSHAKE: u8 = 0x00;
|
||||
}
|
||||
|
||||
pub mod status {
|
||||
pub const CLIENT_STATUS: u8 = 0x0;
|
||||
pub const CLIENT_PING: u8 = 0x01;
|
||||
pub const SERVER_STATUS: u8 = 0x00;
|
||||
pub const SERVER_PING: u8 = 0x01;
|
||||
}
|
||||
|
||||
pub mod login {
|
||||
pub const CLIENT_DISCONNECT: u8 = 0x00;
|
||||
pub const CLIENT_LOGIN_SUCCESS: u8 = 0x02;
|
||||
pub const CLIENT_SET_COMPRESSION: u8 = 0x03;
|
||||
pub const SERVER_LOGIN_START: u8 = 0x00;
|
||||
}
|
||||
|
||||
pub mod play {
|
||||
pub const CLIENT_CHAT_MSG: u8 = 0x0F;
|
||||
pub const CLIENT_PLUGIN_MESSAGE: u8 = 0x18;
|
||||
pub const CLIENT_NAMED_SOUND_EFFECT: u8 = 0x19;
|
||||
pub const CLIENT_DISCONNECT: u8 = 0x1A;
|
||||
pub const CLIENT_KEEP_ALIVE: u8 = 0x21;
|
||||
pub const CLIENT_JOIN_GAME: u8 = 0x26;
|
||||
pub const CLIENT_PLAYER_POS_LOOK: u8 = 0x38;
|
||||
pub const CLIENT_RESPAWN: u8 = 0x3D;
|
||||
pub const CLIENT_SPAWN_POS: u8 = 0x4B;
|
||||
pub const CLIENT_SET_TITLE_SUBTITLE: u8 = 0x57;
|
||||
pub const CLIENT_TIME_UPDATE: u8 = 0x58;
|
||||
pub const CLIENT_SET_TITLE_TEXT: u8 = 0x59;
|
||||
pub const CLIENT_SET_TITLE_TIMES: u8 = 0x5A;
|
||||
pub const SERVER_CLIENT_SETTINGS: u8 = 0x05;
|
||||
pub const SERVER_PLUGIN_MESSAGE: u8 = 0x0A;
|
||||
pub const SERVER_PLAYER_POS: u8 = 0x11;
|
||||
pub const SERVER_PLAYER_POS_ROT: u8 = 0x12;
|
||||
}
|
@ -1,33 +0,0 @@
|
||||
//! 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;
|
||||
}
|
@ -1,208 +0,0 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
@ -1,29 +0,0 @@
|
||||
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,
|
||||
}
|
||||
}
|
@ -1,15 +0,0 @@
|
||||
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;
|
@ -1,48 +0,0 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
@ -1,70 +0,0 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
@ -1,45 +0,0 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
@ -1,48 +0,0 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
@ -1,41 +0,0 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
@ -1,141 +0,0 @@
|
||||
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
|
||||
}
|
125
src/server.rs
125
src/server.rs
@ -14,9 +14,7 @@ use tokio::time;
|
||||
|
||||
use crate::config::{Config, Server as ConfigServer};
|
||||
use crate::mc::ban::{BannedIp, BannedIps};
|
||||
use crate::mc::whitelist::Whitelist;
|
||||
use crate::os;
|
||||
use crate::proto::packets::play::join_game::JoinGameData;
|
||||
|
||||
/// Server cooldown after the process quit.
|
||||
/// Used to give it some more time to quit forgotten threads, such as for RCON.
|
||||
@ -29,11 +27,9 @@ const SERVER_QUIT_COOLDOWN: Duration = Duration::from_millis(2500);
|
||||
#[cfg(feature = "rcon")]
|
||||
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];
|
||||
/// Exit code when SIGTERM is received on Unix.
|
||||
#[cfg(unix)]
|
||||
const UNIX_EXIT_SIGTERM: i32 = 130;
|
||||
|
||||
/// Shared server state.
|
||||
#[derive(Debug)]
|
||||
@ -76,9 +72,6 @@ pub struct Server {
|
||||
/// List of banned IPs.
|
||||
banned_ips: RwLock<BannedIps>,
|
||||
|
||||
/// Whitelist if enabled.
|
||||
whitelist: RwLock<Option<Whitelist>>,
|
||||
|
||||
/// Lock for exclusive RCON operations.
|
||||
#[cfg(feature = "rcon")]
|
||||
rcon_lock: Semaphore,
|
||||
@ -86,14 +79,6 @@ pub struct Server {
|
||||
/// Last time server was stopped over RCON.
|
||||
#[cfg(feature = "rcon")]
|
||||
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 {
|
||||
@ -219,14 +204,9 @@ impl 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
|
||||
Self::spawn_server_task(config, server);
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
@ -242,12 +222,6 @@ impl Server {
|
||||
/// This will attempt to stop the server with all available methods.
|
||||
#[allow(unused_variables)]
|
||||
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
|
||||
#[cfg(feature = "rcon")]
|
||||
if self.state() == State::Started && stop_server_rcon(config, self).await {
|
||||
@ -363,18 +337,6 @@ impl Server {
|
||||
futures::executor::block_on(async { self.is_banned_ip(ip).await })
|
||||
}
|
||||
|
||||
/// Check whether the given username is whitelisted.
|
||||
///
|
||||
/// Returns `true` if no whitelist is currently used.
|
||||
pub async fn is_whitelisted(&self, username: &str) -> bool {
|
||||
self.whitelist
|
||||
.read()
|
||||
.await
|
||||
.as_ref()
|
||||
.map(|w| w.is_whitelisted(username))
|
||||
.unwrap_or(true)
|
||||
}
|
||||
|
||||
/// Update the list of banned IPs.
|
||||
pub async fn set_banned_ips(&self, ips: BannedIps) {
|
||||
*self.banned_ips.write().await = ips;
|
||||
@ -384,16 +346,6 @@ impl Server {
|
||||
pub fn set_banned_ips_blocking(&self, ips: BannedIps) {
|
||||
futures::executor::block_on(async { self.set_banned_ips(ips).await })
|
||||
}
|
||||
|
||||
/// Update the whitelist.
|
||||
pub async fn set_whitelist(&self, whitelist: Option<Whitelist>) {
|
||||
*self.whitelist.write().await = whitelist;
|
||||
}
|
||||
|
||||
/// Update the whitelist.
|
||||
pub fn set_whitelist_blocking(&self, whitelist: Option<Whitelist>) {
|
||||
futures::executor::block_on(async { self.set_whitelist(whitelist).await })
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Server {
|
||||
@ -410,13 +362,10 @@ impl Default for Server {
|
||||
keep_online_until: Default::default(),
|
||||
kill_at: Default::default(),
|
||||
banned_ips: Default::default(),
|
||||
whitelist: Default::default(),
|
||||
#[cfg(feature = "rcon")]
|
||||
rcon_lock: Semaphore::new(1),
|
||||
#[cfg(feature = "rcon")]
|
||||
rcon_last_stop: Default::default(),
|
||||
probed_join_game: Default::default(),
|
||||
forge_payload: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -498,12 +447,8 @@ pub async fn invoke_server_cmd(
|
||||
debug!(target: "lazymc", "Server process stopped successfully ({})", status);
|
||||
false
|
||||
}
|
||||
Ok(status)
|
||||
if status
|
||||
.code()
|
||||
.map(|ref code| ALLOWED_EXIT_CODES.contains(code))
|
||||
.unwrap_or(false) =>
|
||||
{
|
||||
#[cfg(unix)]
|
||||
Ok(status) if status.code() == Some(UNIX_EXIT_SIGTERM) => {
|
||||
debug!(target: "lazymc", "Server process stopped successfully by SIGTERM ({})", status);
|
||||
false
|
||||
}
|
||||
@ -603,11 +548,13 @@ async fn stop_server_signal(config: &Config, server: &Server) -> bool {
|
||||
}
|
||||
};
|
||||
|
||||
// Send kill signal
|
||||
if !crate::os::kill_gracefully(pid) {
|
||||
error!(target: "lazymc", "Failed to send stop signal to server process");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Update from starting/started to stopping
|
||||
server
|
||||
.update_state_from(Some(State::Starting), State::Stopping, config)
|
||||
.await;
|
||||
@ -617,59 +564,3 @@ async fn stop_server_signal(config: &Config, server: &Server) -> bool {
|
||||
|
||||
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
|
||||
}
|
||||
|
119
src/service/ban_reload.rs
Normal file
119
src/service/ban_reload.rs
Normal file
@ -0,0 +1,119 @@
|
||||
use std::path::Path;
|
||||
use std::sync::mpsc::channel;
|
||||
use std::sync::Arc;
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
|
||||
use notify::{watcher, DebouncedEvent, RecursiveMode, Watcher};
|
||||
|
||||
use crate::config::{Config, Server as ConfigServer};
|
||||
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 ConfigServer::server_directory(&config) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,172 +0,0 @@
|
||||
use std::path::Path;
|
||||
use std::sync::mpsc::channel;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use notify::{watcher, DebouncedEvent, RecursiveMode, Watcher};
|
||||
|
||||
use crate::config::{Config, Server as ConfigServer};
|
||||
use crate::mc::ban::{self, BannedIps};
|
||||
use crate::mc::{server_properties, whitelist};
|
||||
use crate::server::Server;
|
||||
|
||||
/// File watcher debounce time.
|
||||
const WATCH_DEBOUNCE: Duration = Duration::from_secs(2);
|
||||
|
||||
/// Service to watch server file changes.
|
||||
pub fn service(config: Arc<Config>, server: Arc<Server>) {
|
||||
// Ensure server directory is set, it must exist
|
||||
let dir = match ConfigServer::server_directory(&config) {
|
||||
Some(dir) if dir.is_dir() => dir,
|
||||
_ => {
|
||||
warn!(target: "lazymc", "Server directory doesn't exist, can't watch file changes to reload whitelist and banned IPs");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
// Keep watching
|
||||
#[allow(clippy::blocks_in_if_conditions)]
|
||||
while {
|
||||
// Update all files once
|
||||
reload_bans(&config, &server, &dir.join(ban::FILE));
|
||||
reload_whitelist(&config, &server, &dir);
|
||||
|
||||
// Watch for changes, update accordingly
|
||||
watch_server(&config, &server, &dir)
|
||||
} {}
|
||||
}
|
||||
|
||||
/// Watch server directory.
|
||||
///
|
||||
/// Returns `true` if we should watch again.
|
||||
#[must_use]
|
||||
fn watch_server(config: &Config, server: &Server, dir: &Path) -> bool {
|
||||
// Directory must exist
|
||||
if !dir.is_dir() {
|
||||
error!(target: "lazymc", "Server directory does not exist at {} anymore, not watching changes", dir.display());
|
||||
return false;
|
||||
}
|
||||
|
||||
// Create watcher for directory
|
||||
let (tx, rx) = channel();
|
||||
let mut watcher =
|
||||
watcher(tx, WATCH_DEBOUNCE).expect("failed to create watcher for banned-ips.json");
|
||||
if let Err(err) = watcher.watch(dir, RecursiveMode::NonRecursive) {
|
||||
error!(target: "lazymc", "An error occured while creating watcher for server files: {}", err);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Handle change events
|
||||
loop {
|
||||
match rx.recv().unwrap() {
|
||||
// Handle file updates
|
||||
DebouncedEvent::Create(ref path)
|
||||
| DebouncedEvent::Write(ref path)
|
||||
| DebouncedEvent::Remove(ref path) => {
|
||||
update(config, server, dir, path);
|
||||
}
|
||||
|
||||
// Handle file updates on both paths for rename
|
||||
DebouncedEvent::Rename(ref before_path, ref after_path) => {
|
||||
update(config, server, dir, before_path);
|
||||
update(config, server, dir, after_path);
|
||||
}
|
||||
|
||||
// Ignore write/remove notices, will receive write/remove event later
|
||||
DebouncedEvent::NoticeWrite(_) | DebouncedEvent::NoticeRemove(_) => {}
|
||||
|
||||
// Ignore chmod changes
|
||||
DebouncedEvent::Chmod(_) => {}
|
||||
|
||||
// Rewatch on rescan
|
||||
DebouncedEvent::Rescan => {
|
||||
debug!(target: "lazymc", "Rescanning server directory files due to file watching problem");
|
||||
return true;
|
||||
}
|
||||
|
||||
// Rewatch on error
|
||||
DebouncedEvent::Error(err, _) => {
|
||||
error!(target: "lazymc", "Error occurred while watching server directory for file changes: {}", err);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Process a file change on the given path.
|
||||
///
|
||||
/// Should be called both when created, changed or removed.
|
||||
fn update(config: &Config, server: &Server, dir: &Path, path: &Path) {
|
||||
// Update bans
|
||||
if path.ends_with(ban::FILE) {
|
||||
reload_bans(config, server, path);
|
||||
}
|
||||
|
||||
// Update whitelist
|
||||
if path.ends_with(whitelist::WHITELIST_FILE)
|
||||
|| path.ends_with(whitelist::OPS_FILE)
|
||||
|| path.ends_with(server_properties::FILE)
|
||||
{
|
||||
reload_whitelist(config, server, dir);
|
||||
}
|
||||
}
|
||||
|
||||
/// Reload banned IPs.
|
||||
fn reload_bans(config: &Config, server: &Server, path: &Path) {
|
||||
// Bans must be enabled
|
||||
if !config.server.block_banned_ips && !config.server.drop_banned_ips {
|
||||
return;
|
||||
}
|
||||
|
||||
trace!(target: "lazymc", "Reloading banned IPs...");
|
||||
|
||||
// File must exist, clear file otherwise
|
||||
if !path.is_file() {
|
||||
debug!(target: "lazymc", "No banned IPs, {} does not exist", ban::FILE);
|
||||
// warn!(target: "lazymc", "Not blocking banned IPs, {} file does not exist", ban::FILE);
|
||||
server.set_banned_ips_blocking(BannedIps::default());
|
||||
return;
|
||||
}
|
||||
|
||||
// Load and update banned IPs
|
||||
match ban::load(path) {
|
||||
Ok(ips) => server.set_banned_ips_blocking(ips),
|
||||
Err(err) => {
|
||||
debug!(target: "lazymc", "Failed load banned IPs from {}, ignoring: {}", ban::FILE, err);
|
||||
}
|
||||
}
|
||||
|
||||
// Show warning if 127.0.0.1 is banned
|
||||
if server.is_banned_ip_blocking(&("127.0.0.1".parse().unwrap())) {
|
||||
warn!(target: "lazymc", "Local address 127.0.0.1 IP banned, probably not what you want");
|
||||
warn!(target: "lazymc", "Use '/pardon-ip 127.0.0.1' on the server to unban");
|
||||
}
|
||||
}
|
||||
|
||||
/// Reload whitelisted users.
|
||||
fn reload_whitelist(config: &Config, server: &Server, dir: &Path) {
|
||||
// Whitelist must be enabled
|
||||
if !config.server.wake_whitelist {
|
||||
return;
|
||||
}
|
||||
|
||||
// Must be enabled in server.properties
|
||||
let enabled = server_properties::read_property(dir.join(server_properties::FILE), "white-list")
|
||||
.map(|v| v.trim() == "true")
|
||||
.unwrap_or(false);
|
||||
if !enabled {
|
||||
server.set_whitelist_blocking(None);
|
||||
debug!(target: "lazymc", "Not using whitelist, not enabled in {}", server_properties::FILE);
|
||||
return;
|
||||
}
|
||||
|
||||
trace!(target: "lazymc", "Reloading whitelisted users...");
|
||||
|
||||
// Load and update whitelisted users
|
||||
match whitelist::load_dir(dir) {
|
||||
Ok(whitelist) => server.set_whitelist_blocking(Some(whitelist)),
|
||||
Err(err) => {
|
||||
debug!(target: "lazymc", "Failed load whitelist from {}, ignoring: {}", dir.display(), err);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,5 +1,4 @@
|
||||
pub mod file_watcher;
|
||||
pub mod ban_reload;
|
||||
pub mod monitor;
|
||||
pub mod probe;
|
||||
pub mod server;
|
||||
pub mod signal;
|
||||
|
@ -1,33 +0,0 @@
|
||||
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
|
||||
}
|
@ -46,22 +46,19 @@ pub async fn service(config: Arc<Config>) -> Result<(), ()> {
|
||||
);
|
||||
}
|
||||
|
||||
// Spawn services: monitor, signal handler
|
||||
// Spawn server monitor and signal handler services
|
||||
tokio::spawn(service::monitor::service(config.clone(), server.clone()));
|
||||
tokio::spawn(service::signal::service(config.clone(), server.clone()));
|
||||
tokio::task::spawn_blocking({
|
||||
let (config, server) = (config.clone(), server.clone());
|
||||
|| service::ban_reload::service(config, server)
|
||||
});
|
||||
|
||||
// Initiate server start
|
||||
if config.server.wake_on_start {
|
||||
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
|
||||
while let Ok((inbound, _)) = listener.accept().await {
|
||||
route(inbound, config.clone(), server.clone());
|
||||
@ -84,7 +81,7 @@ fn route(inbound: TcpStream, config: Arc<Config>, server: Arc<Server>) {
|
||||
|
||||
// Check ban state, just drop connection if enabled
|
||||
let banned = server.is_banned_ip_blocking(&peer.ip());
|
||||
if banned && config.server.drop_banned_ips {
|
||||
if config.server.drop_banned_ips {
|
||||
info!(target: "lazymc", "Connection from banned IP {}, dropping", peer.ip());
|
||||
return;
|
||||
}
|
||||
|
@ -27,9 +27,6 @@ const BAN_MESSAGE_PREFIX: &str = "Your IP address is banned from this server.\nR
|
||||
/// Default ban reason if unknown.
|
||||
const DEFAULT_BAN_REASON: &str = "Banned by an operator.";
|
||||
|
||||
/// The not-whitelisted kick message.
|
||||
const WHITELIST_MESSAGE: &str = "You are not white-listed on this server!";
|
||||
|
||||
/// Server icon file path.
|
||||
const SERVER_ICON_FILE: &str = "server-icon.png";
|
||||
|
||||
@ -88,9 +85,8 @@ pub async fn serve(
|
||||
|
||||
// Update client info and client state
|
||||
client_info
|
||||
.protocol
|
||||
.replace(handshake.protocol_version as u32);
|
||||
client_info.handshake.replace(handshake);
|
||||
.protocol_version
|
||||
.replace(handshake.protocol_version);
|
||||
client.set_state(new_state);
|
||||
|
||||
// If loggin in with handshake, remember inbound
|
||||
@ -109,7 +105,7 @@ pub async fn serve(
|
||||
let mut data = Vec::new();
|
||||
packet.encode(&mut data).map_err(|_| ())?;
|
||||
|
||||
let response = RawPacket::new(0, data).encode_with_len(&client)?;
|
||||
let response = RawPacket::new(0, data).encode(&client)?;
|
||||
writer.write_all(&response).await.map_err(|_| ())?;
|
||||
|
||||
continue;
|
||||
@ -152,17 +148,12 @@ pub async fn serve(
|
||||
info!(target: "lazymc", "Login from banned IP {}, disconnecting", client.peer.ip());
|
||||
DEFAULT_BAN_REASON.to_string()
|
||||
};
|
||||
action::kick(&client, &format!("{BAN_MESSAGE_PREFIX}{msg}"), &mut writer)
|
||||
.await?;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Kick if client is not whitelisted to wake server
|
||||
if let Some(ref username) = username {
|
||||
if !server.is_whitelisted(username).await {
|
||||
info!(target: "lazymc", "User '{}' tried to wake server but is not whitelisted, disconnecting", username);
|
||||
action::kick(&client, WHITELIST_MESSAGE, &mut writer).await?;
|
||||
action::kick(
|
||||
&client,
|
||||
&format!("{}{}", BAN_MESSAGE_PREFIX, msg),
|
||||
&mut writer,
|
||||
)
|
||||
.await?;
|
||||
break;
|
||||
}
|
||||
}
|
||||
@ -197,7 +188,7 @@ pub async fn serve(
|
||||
}
|
||||
|
||||
// Show unhandled packet warning
|
||||
debug!(target: "lazymc", "Got unhandled packet:");
|
||||
debug!(target: "lazymc", "Received unhandled packet:");
|
||||
debug!(target: "lazymc", "- State: {:?}", client_state);
|
||||
debug!(target: "lazymc", "- Packet ID: {}", packet.id);
|
||||
}
|
||||
|
@ -10,7 +10,7 @@ use crate::util::error::{quit_error, ErrorHints};
|
||||
/// excluding the `:` suffix.
|
||||
pub fn prompt(msg: &str) -> String {
|
||||
// Show the prompt
|
||||
eprint!("{msg}: ");
|
||||
eprint!("{}: ", msg);
|
||||
let _ = stderr().flush();
|
||||
|
||||
// Get the input
|
||||
@ -49,7 +49,7 @@ pub fn prompt_yes(msg: &str, def: Option<bool>) -> bool {
|
||||
);
|
||||
|
||||
// Get the user input
|
||||
let answer = prompt(&format!("{msg} {options}"));
|
||||
let answer = prompt(&format!("{} {}", msg, options));
|
||||
|
||||
// Assume the default if the answer is empty
|
||||
if answer.is_empty() {
|
||||
|
@ -16,7 +16,7 @@ pub fn print_error(err: anyhow::Error) {
|
||||
// Report each printable error, count them
|
||||
let count = err
|
||||
.chain()
|
||||
.map(|err| err.to_string())
|
||||
.map(|err| format!("{}", err))
|
||||
.filter(|err| !err.is_empty())
|
||||
.enumerate()
|
||||
.map(|(i, err)| {
|
||||
@ -126,7 +126,7 @@ impl ErrorHints {
|
||||
if self.config_generate {
|
||||
eprintln!(
|
||||
"Use '{}' to generate a new config file",
|
||||
highlight(&format!("{bin} config generate"))
|
||||
highlight(&format!("{} config generate", bin))
|
||||
);
|
||||
}
|
||||
if self.config {
|
||||
@ -138,7 +138,7 @@ impl ErrorHints {
|
||||
if self.config_test {
|
||||
eprintln!(
|
||||
"Use '{}' to test a config file",
|
||||
highlight(&format!("{bin} config test -c FILE"))
|
||||
highlight(&format!("{} config test -c FILE", bin))
|
||||
);
|
||||
}
|
||||
if self.verbose {
|
||||
|
Loading…
x
Reference in New Issue
Block a user