Server projects post launch fixes (#5481)
* Vendor async-minecraft-ping and fix servers returning protocol version -1 * Don't have automod reject server projects * fmt * Add region to search facets * remove AMP .github
This commit is contained in:
121
Cargo.lock
generated
121
Cargo.lock
generated
@@ -378,6 +378,15 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ansi_term"
|
||||
version = "0.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2"
|
||||
dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstream"
|
||||
version = "0.6.21"
|
||||
@@ -643,12 +652,13 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "async-minecraft-ping"
|
||||
version = "0.8.0"
|
||||
source = "git+https://github.com/jsvana/async-minecraft-ping?rev=56a64a8a59de854fb81cc0f9f66c2285873d960c#56a64a8a59de854fb81cc0f9f66c2285873d960c"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
"hickory-resolver 0.24.4",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"structopt",
|
||||
"thiserror 1.0.69",
|
||||
"tokio",
|
||||
]
|
||||
@@ -864,6 +874,17 @@ dependencies = [
|
||||
"webpki-roots 1.0.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "atty"
|
||||
version = "0.2.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
|
||||
dependencies = [
|
||||
"hermit-abi 0.1.19",
|
||||
"libc",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "autocfg"
|
||||
version = "1.5.0"
|
||||
@@ -1581,6 +1602,21 @@ dependencies = [
|
||||
"libloading 0.8.8",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "2.34.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c"
|
||||
dependencies = [
|
||||
"ansi_term",
|
||||
"atty",
|
||||
"bitflags 1.3.2",
|
||||
"strsim 0.8.0",
|
||||
"textwrap",
|
||||
"unicode-width 0.1.14",
|
||||
"vec_map",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.5.48"
|
||||
@@ -1600,7 +1636,7 @@ dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
"clap_lex",
|
||||
"strsim",
|
||||
"strsim 0.11.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2200,7 +2236,7 @@ dependencies = [
|
||||
"ident_case",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"strsim",
|
||||
"strsim 0.11.1",
|
||||
"syn 2.0.106",
|
||||
]
|
||||
|
||||
@@ -2214,7 +2250,7 @@ dependencies = [
|
||||
"ident_case",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"strsim",
|
||||
"strsim 0.11.1",
|
||||
"syn 2.0.106",
|
||||
]
|
||||
|
||||
@@ -2227,7 +2263,7 @@ dependencies = [
|
||||
"ident_case",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"strsim",
|
||||
"strsim 0.11.1",
|
||||
"syn 2.0.106",
|
||||
]
|
||||
|
||||
@@ -3764,6 +3800,15 @@ dependencies = [
|
||||
"hashbrown 0.15.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
version = "0.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c"
|
||||
dependencies = [
|
||||
"unicode-segmentation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
version = "0.4.1"
|
||||
@@ -3776,6 +3821,15 @@ version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||
|
||||
[[package]]
|
||||
name = "hermit-abi"
|
||||
version = "0.1.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hermit-abi"
|
||||
version = "0.5.2"
|
||||
@@ -4447,7 +4501,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e96d2465363ed2d81857759fc864cf6bb7997f79327aec028d65bd7989393685"
|
||||
dependencies = [
|
||||
"ahash 0.8.12",
|
||||
"clap",
|
||||
"clap 4.5.48",
|
||||
"crossbeam-channel",
|
||||
"crossbeam-utils",
|
||||
"dashmap",
|
||||
@@ -4562,7 +4616,7 @@ version = "0.4.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9"
|
||||
dependencies = [
|
||||
"hermit-abi",
|
||||
"hermit-abi 0.5.2",
|
||||
"libc",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
@@ -4830,7 +4884,7 @@ dependencies = [
|
||||
"bytes",
|
||||
"censor",
|
||||
"chrono",
|
||||
"clap",
|
||||
"clap 4.5.48",
|
||||
"clickhouse",
|
||||
"color-eyre",
|
||||
"color-thief",
|
||||
@@ -5418,7 +5472,7 @@ name = "modrinth-maxmind"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"clap",
|
||||
"clap 4.5.48",
|
||||
"directories",
|
||||
"eyre",
|
||||
"flate2",
|
||||
@@ -5826,7 +5880,7 @@ version = "1.17.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b"
|
||||
dependencies = [
|
||||
"hermit-abi",
|
||||
"hermit-abi 0.5.2",
|
||||
"libc",
|
||||
]
|
||||
|
||||
@@ -6834,7 +6888,7 @@ checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"concurrent-queue",
|
||||
"hermit-abi",
|
||||
"hermit-abi 0.5.2",
|
||||
"pin-project-lite",
|
||||
"rustix 1.1.2",
|
||||
"windows-sys 0.61.2",
|
||||
@@ -9312,6 +9366,12 @@ dependencies = [
|
||||
"unicode-properties",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "strsim"
|
||||
version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a"
|
||||
|
||||
[[package]]
|
||||
name = "strsim"
|
||||
version = "0.11.1"
|
||||
@@ -9341,6 +9401,30 @@ dependencies = [
|
||||
"syn 2.0.106",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "structopt"
|
||||
version = "0.3.26"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0c6b5c64445ba8094a6ab0c3cd2ad323e07171012d9c98b0b15651daf1787a10"
|
||||
dependencies = [
|
||||
"clap 2.34.0",
|
||||
"lazy_static",
|
||||
"structopt-derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "structopt-derive"
|
||||
version = "0.4.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dcb5ae327f9cc13b68763b5749770cb9e048a99bd9dfdfa58d0cf05d5f64afe0"
|
||||
dependencies = [
|
||||
"heck 0.3.3",
|
||||
"proc-macro-error",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 1.0.109",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "strum"
|
||||
version = "0.27.2"
|
||||
@@ -10043,6 +10127,15 @@ dependencies = [
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "textwrap"
|
||||
version = "0.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060"
|
||||
dependencies = [
|
||||
"unicode-width 0.1.14",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "theseus"
|
||||
version = "1.0.0-local"
|
||||
@@ -11184,6 +11277,12 @@ version = "0.2.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
|
||||
|
||||
[[package]]
|
||||
name = "vec_map"
|
||||
version = "0.8.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191"
|
||||
|
||||
[[package]]
|
||||
name = "version-compare"
|
||||
version = "0.2.0"
|
||||
|
||||
@@ -33,7 +33,7 @@ arc-swap = "1.7.1"
|
||||
argon2 = { version = "0.5.3", features = ["std"] }
|
||||
ariadne = { path = "packages/ariadne" }
|
||||
async-compression = { version = "0.4.32", default-features = false }
|
||||
async-minecraft-ping = { git = "https://github.com/jsvana/async-minecraft-ping", rev = "56a64a8a59de854fb81cc0f9f66c2285873d960c" }
|
||||
async-minecraft-ping = { path = "packages/async-minecraft-ping" }
|
||||
async-recursion = "1.1.1"
|
||||
async-stripe = { version = "0.41.0", default-features = false, features = [
|
||||
"runtime-tokio-hyper-rustls",
|
||||
|
||||
@@ -176,9 +176,9 @@ pub async fn init_client_with_database(
|
||||
latency_ms Nullable(UInt32),
|
||||
description Nullable(String),
|
||||
version_name Nullable(String),
|
||||
version_protocol Nullable(UInt32),
|
||||
players_online Nullable(UInt32),
|
||||
players_max Nullable(UInt32)
|
||||
version_protocol Nullable(Int32),
|
||||
players_online Nullable(Int32),
|
||||
players_max Nullable(Int32)
|
||||
)
|
||||
ENGINE = {engine}
|
||||
{ttl}
|
||||
|
||||
@@ -387,13 +387,13 @@ pub struct JavaServerPingData {
|
||||
/// Reported version name of the server.
|
||||
pub version_name: String,
|
||||
/// Reported version protocol number of the server.
|
||||
pub version_protocol: u32,
|
||||
pub version_protocol: i32,
|
||||
/// Description/MOTD of the server as shown in the server list.
|
||||
pub description: String,
|
||||
/// Number of players online at the time.
|
||||
pub players_online: u32,
|
||||
pub players_online: i32,
|
||||
/// Maximum number of players allowed on the server.
|
||||
pub players_max: u32,
|
||||
pub players_max: i32,
|
||||
}
|
||||
|
||||
component::relations! {
|
||||
|
||||
@@ -656,7 +656,7 @@ impl AutomatedModerationQueue {
|
||||
)
|
||||
.await?;
|
||||
|
||||
if mod_messages.should_reject(first_time) {
|
||||
if mod_messages.should_reject(first_time) && !is_server_project {
|
||||
ThreadMessageBuilder {
|
||||
author_id: Some(database::models::DBUserId(AUTOMOD_ID)),
|
||||
body: MessageBody::StatusChange {
|
||||
|
||||
@@ -305,9 +305,9 @@ struct ServerPingRecord {
|
||||
latency_ms: Option<u32>,
|
||||
description: Option<String>,
|
||||
version_name: Option<String>,
|
||||
version_protocol: Option<u32>,
|
||||
players_online: Option<u32>,
|
||||
players_max: Option<u32>,
|
||||
version_protocol: Option<i32>,
|
||||
players_online: Option<i32>,
|
||||
players_max: Option<i32>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -641,6 +641,7 @@ const DEFAULT_ATTRIBUTES_FOR_FACETING: &[&str] = &[
|
||||
"client_side",
|
||||
"server_side",
|
||||
"minecraft_server.country",
|
||||
"minecraft_server.region",
|
||||
"minecraft_server.languages",
|
||||
"minecraft_java_server.content.kind",
|
||||
"minecraft_java_server.content.supported_game_versions",
|
||||
|
||||
@@ -973,8 +973,8 @@ async fn _get_server_status_new(
|
||||
};
|
||||
|
||||
let players = ServerPlayers {
|
||||
max: status.players.max.cast_signed(),
|
||||
online: status.players.online.cast_signed(),
|
||||
max: status.players.max,
|
||||
online: status.players.online,
|
||||
sample: status
|
||||
.players
|
||||
.sample
|
||||
|
||||
@@ -43,7 +43,7 @@ pub struct ServerGameProfile {
|
||||
#[derive(Deserialize, Serialize, Debug, Clone)]
|
||||
pub struct ServerVersion {
|
||||
pub name: String,
|
||||
pub protocol: u32,
|
||||
pub protocol: i32,
|
||||
#[serde(skip_deserializing)]
|
||||
pub legacy: bool,
|
||||
}
|
||||
|
||||
2
packages/async-minecraft-ping/.gitignore
vendored
Normal file
2
packages/async-minecraft-ping/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
/target
|
||||
Cargo.lock
|
||||
1
packages/async-minecraft-ping/.rustfmt.toml
Normal file
1
packages/async-minecraft-ping/.rustfmt.toml
Normal file
@@ -0,0 +1 @@
|
||||
edition = "2018"
|
||||
34
packages/async-minecraft-ping/Cargo.toml
Normal file
34
packages/async-minecraft-ping/Cargo.toml
Normal file
@@ -0,0 +1,34 @@
|
||||
[package]
|
||||
name = "async-minecraft-ping"
|
||||
version = "0.8.0"
|
||||
authors = ["Jay Vana <jaysvana@gmail.com>"]
|
||||
edition = "2021"
|
||||
license = "MIT OR Apache-2.0"
|
||||
description = "An async Rust client for the Minecraft ServerListPing protocol"
|
||||
readme = "README.md"
|
||||
repository = "https://github.com/jsvana/async-minecraft-ping/"
|
||||
keywords = ["mc", "minecraft", "serverlistping"]
|
||||
categories = ["api-bindings", "asynchronous"]
|
||||
|
||||
[dependencies]
|
||||
async-trait = "0.1"
|
||||
hickory-resolver = { version = "0.24", optional = true }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
thiserror = "1.0"
|
||||
|
||||
[dependencies.tokio]
|
||||
version = "1.15"
|
||||
features = ["io-util", "net", "time"]
|
||||
|
||||
[dev-dependencies]
|
||||
anyhow = "1.0"
|
||||
structopt = "0.3"
|
||||
|
||||
[dev-dependencies.tokio]
|
||||
version = "1.15"
|
||||
features = ["io-util", "macros", "net", "rt-multi-thread"]
|
||||
|
||||
[features]
|
||||
default = []
|
||||
srv = ["hickory-resolver"]
|
||||
201
packages/async-minecraft-ping/LICENSE-APACHE
Normal file
201
packages/async-minecraft-ping/LICENSE-APACHE
Normal file
@@ -0,0 +1,201 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
23
packages/async-minecraft-ping/LICENSE-MIT
Normal file
23
packages/async-minecraft-ping/LICENSE-MIT
Normal file
@@ -0,0 +1,23 @@
|
||||
Permission is hereby granted, free of charge, to any
|
||||
person obtaining a copy of this software and associated
|
||||
documentation files (the "Software"), to deal in the
|
||||
Software without restriction, including without
|
||||
limitation the rights to use, copy, modify, merge,
|
||||
publish, distribute, sublicense, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software
|
||||
is furnished to do so, subject to the following
|
||||
conditions:
|
||||
|
||||
The above copyright notice and this permission notice
|
||||
shall be included in all copies or substantial portions
|
||||
of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
|
||||
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
|
||||
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
|
||||
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
|
||||
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
|
||||
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
72
packages/async-minecraft-ping/README.md
Normal file
72
packages/async-minecraft-ping/README.md
Normal file
@@ -0,0 +1,72 @@
|
||||
> [!NOTE]
|
||||
>
|
||||
> This is a vendored version of [`async-minecraft-ping`](https://github.com/jsvana/async-minecraft-ping) with modifications for our own purposes.
|
||||
>
|
||||
> This directory is licensed under the same license as the original project.
|
||||
|
||||
# async-minecraft-ping
|
||||
|
||||
[][crate]
|
||||
[][docs]
|
||||
[](https://github.com/jsvana/async-minecraft-ping/actions/workflows/ci.yml)
|
||||

|
||||
|
||||
An async [ServerListPing](https://wiki.vg/Server_List_Ping) client implementation in Rust.
|
||||
|
||||
## Usage
|
||||
|
||||
See [the example](./examples/status.rs).
|
||||
|
||||
```rust
|
||||
let config = ConnectionConfig::build("mc.example.com")
|
||||
.with_port(25565)
|
||||
.with_timeout(Duration::from_secs(5));
|
||||
|
||||
let connection = config.connect().await?;
|
||||
let status = connection.status().await?;
|
||||
|
||||
println!(
|
||||
"{} of {} player(s) online",
|
||||
status.status.players.online, status.status.players.max
|
||||
);
|
||||
```
|
||||
|
||||
## Features
|
||||
|
||||
### SRV Record Lookup
|
||||
|
||||
Enable the `srv` feature to support automatic SRV record resolution for Minecraft servers:
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
async-minecraft-ping = { version = "0.8", features = ["srv"] }
|
||||
```
|
||||
|
||||
```rust
|
||||
let config = ConnectionConfig::build("skyblock.net")
|
||||
.with_srv_lookup() // Resolves _minecraft._tcp.skyblock.net
|
||||
.connect()
|
||||
.await?;
|
||||
```
|
||||
|
||||
When SRV lookup is enabled, the library queries `_minecraft._tcp.<address>` for an SRV record. If found, it uses the target host and port from the record. If not found, it falls back to the original address and port.
|
||||
|
||||
## License
|
||||
|
||||
Licensed under either of
|
||||
|
||||
* Apache License, Version 2.0
|
||||
([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0)
|
||||
* MIT license
|
||||
([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT)
|
||||
|
||||
at your option.
|
||||
|
||||
## Contribution
|
||||
|
||||
Unless you explicitly state otherwise, any contribution intentionally submitted
|
||||
for inclusion in the work by you, as defined in the Apache-2.0 license, shall be
|
||||
dual licensed as above, without any additional terms or conditions.
|
||||
|
||||
[crate]: https://crates.io/crates/async-minecraft-ping
|
||||
[docs]: https://docs.rs/async-minecraft-ping
|
||||
48
packages/async-minecraft-ping/examples/status.rs
Normal file
48
packages/async-minecraft-ping/examples/status.rs
Normal file
@@ -0,0 +1,48 @@
|
||||
use anyhow::Result;
|
||||
use structopt::StructOpt;
|
||||
|
||||
use async_minecraft_ping::ConnectionConfig;
|
||||
|
||||
#[derive(Debug, StructOpt)]
|
||||
#[structopt(name = "example")]
|
||||
struct Args {
|
||||
/// Server to connect to
|
||||
#[structopt()]
|
||||
address: String,
|
||||
|
||||
/// Port to connect to
|
||||
#[structopt(short = "p", long = "port")]
|
||||
port: Option<u16>,
|
||||
|
||||
/// Enable SRV record lookup (requires 'srv' feature)
|
||||
#[cfg(feature = "srv")]
|
||||
#[structopt(long = "srv")]
|
||||
srv_lookup: bool,
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
let args = Args::from_args();
|
||||
|
||||
let mut config = ConnectionConfig::build(args.address);
|
||||
if let Some(port) = args.port {
|
||||
config = config.with_port(port);
|
||||
}
|
||||
#[cfg(feature = "srv")]
|
||||
if args.srv_lookup {
|
||||
config = config.with_srv_lookup();
|
||||
}
|
||||
|
||||
let connection = config.connect().await?;
|
||||
|
||||
let connection = connection.status().await?;
|
||||
|
||||
println!(
|
||||
"{} of {} player(s) online",
|
||||
connection.status.players.online, connection.status.players.max
|
||||
);
|
||||
|
||||
connection.ping(42).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
6
packages/async-minecraft-ping/src/lib.rs
Normal file
6
packages/async-minecraft-ping/src/lib.rs
Normal file
@@ -0,0 +1,6 @@
|
||||
mod protocol;
|
||||
mod server;
|
||||
pub use server::{
|
||||
connect, ConnectionConfig, ServerDescription, ServerError, ServerPlayer, ServerPlayers,
|
||||
ServerVersion, StatusConnection, StatusResponse,
|
||||
};
|
||||
576
packages/async-minecraft-ping/src/protocol.rs
Normal file
576
packages/async-minecraft-ping/src/protocol.rs
Normal file
@@ -0,0 +1,576 @@
|
||||
//! This module defines various methods to read and
|
||||
//! write packets in Minecraft's
|
||||
//! [ServerListPing](https://wiki.vg/Server_List_Ping)
|
||||
//! protocol.
|
||||
|
||||
use std::io::Cursor;
|
||||
use std::time::Duration;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use thiserror::Error;
|
||||
use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum ProtocolError {
|
||||
#[error("error reading or writing data")]
|
||||
Io(#[from] std::io::Error),
|
||||
|
||||
#[error("invalid packet length")]
|
||||
InvalidPacketLength,
|
||||
|
||||
#[error("invalid varint data")]
|
||||
InvalidVarInt,
|
||||
|
||||
#[error("invalid packet (expected ID {expected:?}, actual ID {actual:?})")]
|
||||
InvalidPacketId { expected: usize, actual: usize },
|
||||
|
||||
#[error("invalid ServerListPing response body (invalid UTF-8)")]
|
||||
InvalidResponseBody,
|
||||
|
||||
#[error("connection timed out")]
|
||||
Timeout(#[from] tokio::time::error::Elapsed),
|
||||
}
|
||||
|
||||
/// State represents the desired next state of the
|
||||
/// exchange.
|
||||
///
|
||||
/// It's a bit silly now as there's only
|
||||
/// one entry, but technically there is more than
|
||||
/// one type that can be sent here.
|
||||
#[derive(Clone, Copy)]
|
||||
pub enum State {
|
||||
Status,
|
||||
}
|
||||
|
||||
impl From<State> for usize {
|
||||
fn from(state: State) -> Self {
|
||||
match state {
|
||||
State::Status => 1,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// RawPacket is the underlying wrapper of data that
|
||||
/// gets read from and written to the socket.
|
||||
///
|
||||
/// Typically, the flow looks like this:
|
||||
/// 1. Construct a specific packet (HandshakePacket
|
||||
/// for example).
|
||||
/// 2. Write that packet's contents to a byte buffer.
|
||||
/// 3. Construct a RawPacket using that byte buffer.
|
||||
/// 4. Write the RawPacket to the socket.
|
||||
struct RawPacket {
|
||||
id: usize,
|
||||
data: Box<[u8]>,
|
||||
}
|
||||
|
||||
impl RawPacket {
|
||||
fn new(id: usize, data: Box<[u8]>) -> Self {
|
||||
RawPacket { id, data }
|
||||
}
|
||||
}
|
||||
|
||||
/// AsyncWireReadExt adds varint and varint-backed
|
||||
/// string support to things that implement AsyncRead.
|
||||
#[async_trait]
|
||||
pub trait AsyncWireReadExt {
|
||||
async fn read_varint(&mut self) -> Result<usize, ProtocolError>;
|
||||
async fn read_string(&mut self) -> Result<String, ProtocolError>;
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<R: AsyncRead + Unpin + Send + Sync> AsyncWireReadExt for R {
|
||||
async fn read_varint(&mut self) -> Result<usize, ProtocolError> {
|
||||
let mut read = 0;
|
||||
let mut result = 0;
|
||||
loop {
|
||||
let read_value = self.read_u8().await?;
|
||||
let value = read_value & 0b0111_1111;
|
||||
result |= (value as usize) << (7 * read);
|
||||
read += 1;
|
||||
if read > 5 {
|
||||
return Err(ProtocolError::InvalidVarInt);
|
||||
}
|
||||
if (read_value & 0b1000_0000) == 0 {
|
||||
return Ok(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn read_string(&mut self) -> Result<String, ProtocolError> {
|
||||
let length = self.read_varint().await?;
|
||||
|
||||
let mut buffer = vec![0; length];
|
||||
self.read_exact(&mut buffer).await?;
|
||||
|
||||
Ok(String::from_utf8(buffer).map_err(|_| ProtocolError::InvalidResponseBody)?)
|
||||
}
|
||||
}
|
||||
|
||||
/// AsyncWireWriteExt adds varint and varint-backed
|
||||
/// string support to things that implement AsyncWrite.
|
||||
#[async_trait]
|
||||
pub trait AsyncWireWriteExt {
|
||||
async fn write_varint(&mut self, int: usize) -> Result<(), ProtocolError>;
|
||||
async fn write_string(&mut self, string: &str) -> Result<(), ProtocolError>;
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<W: AsyncWrite + Unpin + Send + Sync> AsyncWireWriteExt for W {
|
||||
async fn write_varint(&mut self, int: usize) -> Result<(), ProtocolError> {
|
||||
let mut int = (int as u64) & 0xFFFF_FFFF;
|
||||
let mut written = 0;
|
||||
let mut buffer = [0; 5];
|
||||
loop {
|
||||
let temp = (int & 0b0111_1111) as u8;
|
||||
int >>= 7;
|
||||
if int != 0 {
|
||||
buffer[written] = temp | 0b1000_0000;
|
||||
} else {
|
||||
buffer[written] = temp;
|
||||
}
|
||||
written += 1;
|
||||
if int == 0 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
self.write_all(&buffer[0..written]).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn write_string(&mut self, string: &str) -> Result<(), ProtocolError> {
|
||||
self.write_varint(string.len()).await?;
|
||||
self.write_all(string.as_bytes()).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// PacketId is used to allow AsyncWriteRawPacket
|
||||
/// to generically get a packet's ID.
|
||||
pub trait PacketId {
|
||||
fn get_packet_id(&self) -> usize;
|
||||
}
|
||||
|
||||
/// ExpectedPacketId is used to allow AsyncReadRawPacket
|
||||
/// to generically get a packet's expected ID.
|
||||
pub trait ExpectedPacketId {
|
||||
fn get_expected_packet_id() -> usize;
|
||||
}
|
||||
|
||||
/// AsyncReadFromBuffer is used to allow
|
||||
/// AsyncReadRawPacket to generically read a
|
||||
/// packet's specific data from a buffer.
|
||||
#[async_trait]
|
||||
pub trait AsyncReadFromBuffer: Sized {
|
||||
async fn read_from_buffer(buffer: Vec<u8>) -> Result<Self, ProtocolError>;
|
||||
}
|
||||
|
||||
/// AsyncWriteToBuffer is used to allow
|
||||
/// AsyncWriteRawPacket to generically write a
|
||||
/// packet's specific data into a buffer.
|
||||
#[async_trait]
|
||||
pub trait AsyncWriteToBuffer {
|
||||
async fn write_to_buffer(&self) -> Result<Vec<u8>, ProtocolError>;
|
||||
}
|
||||
|
||||
/// AsyncReadRawPacket is the core piece of
|
||||
/// the read side of the protocol. It allows
|
||||
/// the user to construct a specific packet
|
||||
/// from something that implements AsyncRead.
|
||||
#[async_trait]
|
||||
pub trait AsyncReadRawPacket {
|
||||
async fn read_packet<T: ExpectedPacketId + AsyncReadFromBuffer + Send + Sync>(
|
||||
&mut self,
|
||||
) -> Result<T, ProtocolError>;
|
||||
|
||||
async fn read_packet_with_timeout<T: ExpectedPacketId + AsyncReadFromBuffer + Send + Sync>(
|
||||
&mut self,
|
||||
timeout: Duration,
|
||||
) -> Result<T, ProtocolError>;
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<R: AsyncRead + Unpin + Send + Sync> AsyncReadRawPacket for R {
|
||||
async fn read_packet<T: ExpectedPacketId + AsyncReadFromBuffer + Send + Sync>(
|
||||
&mut self,
|
||||
) -> Result<T, ProtocolError> {
|
||||
let length = self.read_varint().await?;
|
||||
|
||||
if length == 0 {
|
||||
return Err(ProtocolError::InvalidPacketLength);
|
||||
}
|
||||
|
||||
let packet_id = self.read_varint().await?;
|
||||
|
||||
let expected_packet_id = T::get_expected_packet_id();
|
||||
|
||||
if packet_id != expected_packet_id {
|
||||
return Err(ProtocolError::InvalidPacketId {
|
||||
expected: expected_packet_id,
|
||||
actual: packet_id,
|
||||
});
|
||||
}
|
||||
|
||||
let mut buffer = vec![0; length - 1];
|
||||
self.read_exact(&mut buffer).await?;
|
||||
|
||||
T::read_from_buffer(buffer).await
|
||||
}
|
||||
|
||||
async fn read_packet_with_timeout<T: ExpectedPacketId + AsyncReadFromBuffer + Send + Sync>(
|
||||
&mut self,
|
||||
timeout: Duration,
|
||||
) -> Result<T, ProtocolError> {
|
||||
tokio::time::timeout(timeout, self.read_packet()).await?
|
||||
}
|
||||
}
|
||||
|
||||
/// AsyncWriteRawPacket is the core piece of
|
||||
/// the write side of the protocol. It allows
|
||||
/// the user to write a specific packet to
|
||||
/// something that implements AsyncWrite.
|
||||
#[async_trait]
|
||||
pub trait AsyncWriteRawPacket {
|
||||
async fn write_packet<T: PacketId + AsyncWriteToBuffer + Send + Sync>(
|
||||
&mut self,
|
||||
packet: T,
|
||||
) -> Result<(), ProtocolError>;
|
||||
|
||||
async fn write_packet_with_timeout<T: PacketId + AsyncWriteToBuffer + Send + Sync>(
|
||||
&mut self,
|
||||
packet: T,
|
||||
timeout: Duration,
|
||||
) -> Result<(), ProtocolError>;
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<W: AsyncWrite + Unpin + Send + Sync> AsyncWriteRawPacket for W {
|
||||
async fn write_packet<T: PacketId + AsyncWriteToBuffer + Send + Sync>(
|
||||
&mut self,
|
||||
packet: T,
|
||||
) -> Result<(), ProtocolError> {
|
||||
let packet_buffer = packet.write_to_buffer().await?;
|
||||
|
||||
let raw_packet = RawPacket::new(packet.get_packet_id(), packet_buffer.into_boxed_slice());
|
||||
|
||||
let mut buffer: Cursor<Vec<u8>> = Cursor::new(Vec::new());
|
||||
|
||||
buffer.write_varint(raw_packet.id).await?;
|
||||
buffer.write_all(&raw_packet.data).await?;
|
||||
|
||||
let inner = buffer.into_inner();
|
||||
self.write_varint(inner.len()).await?;
|
||||
self.write_all(&inner).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn write_packet_with_timeout<T: PacketId + AsyncWriteToBuffer + Send + Sync>(
|
||||
&mut self,
|
||||
packet: T,
|
||||
timeout: Duration,
|
||||
) -> Result<(), ProtocolError> {
|
||||
tokio::time::timeout(timeout, self.write_packet(packet)).await?
|
||||
}
|
||||
}
|
||||
|
||||
/// HandshakePacket is the first of two packets
|
||||
/// to be sent during a status check for
|
||||
/// ServerListPing.
|
||||
pub struct HandshakePacket {
|
||||
pub packet_id: usize,
|
||||
pub protocol_version: usize,
|
||||
pub server_address: String,
|
||||
pub server_port: u16,
|
||||
pub next_state: State,
|
||||
}
|
||||
|
||||
impl HandshakePacket {
|
||||
pub fn new(protocol_version: usize, server_address: String, server_port: u16) -> Self {
|
||||
Self {
|
||||
packet_id: 0,
|
||||
protocol_version,
|
||||
server_address,
|
||||
server_port,
|
||||
next_state: State::Status,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl AsyncWriteToBuffer for HandshakePacket {
|
||||
async fn write_to_buffer(&self) -> Result<Vec<u8>, ProtocolError> {
|
||||
let mut buffer = Cursor::new(Vec::<u8>::new());
|
||||
|
||||
buffer.write_varint(self.protocol_version).await?;
|
||||
buffer.write_string(&self.server_address).await?;
|
||||
buffer.write_u16(self.server_port).await?;
|
||||
buffer.write_varint(self.next_state.into()).await?;
|
||||
|
||||
Ok(buffer.into_inner())
|
||||
}
|
||||
}
|
||||
|
||||
impl PacketId for HandshakePacket {
|
||||
fn get_packet_id(&self) -> usize {
|
||||
self.packet_id
|
||||
}
|
||||
}
|
||||
|
||||
/// RequestPacket is the second of two packets
|
||||
/// to be sent during a status check for
|
||||
/// ServerListPing.
|
||||
pub struct RequestPacket {
|
||||
pub packet_id: usize,
|
||||
}
|
||||
|
||||
impl RequestPacket {
|
||||
pub fn new() -> Self {
|
||||
Self { packet_id: 0 }
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl AsyncWriteToBuffer for RequestPacket {
|
||||
async fn write_to_buffer(&self) -> Result<Vec<u8>, ProtocolError> {
|
||||
Ok(Vec::new())
|
||||
}
|
||||
}
|
||||
|
||||
impl PacketId for RequestPacket {
|
||||
fn get_packet_id(&self) -> usize {
|
||||
self.packet_id
|
||||
}
|
||||
}
|
||||
|
||||
/// ResponsePacket is the response from the
|
||||
/// server to a status check for
|
||||
/// ServerListPing.
|
||||
pub struct ResponsePacket {
|
||||
#[allow(dead_code)]
|
||||
pub packet_id: usize,
|
||||
pub body: String,
|
||||
}
|
||||
|
||||
impl ExpectedPacketId for ResponsePacket {
|
||||
fn get_expected_packet_id() -> usize {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl AsyncReadFromBuffer for ResponsePacket {
|
||||
async fn read_from_buffer(buffer: Vec<u8>) -> Result<Self, ProtocolError> {
|
||||
let mut reader = Cursor::new(buffer);
|
||||
|
||||
let body = reader.read_string().await?;
|
||||
|
||||
Ok(ResponsePacket { packet_id: 0, body })
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PingPacket {
|
||||
pub packet_id: usize,
|
||||
pub payload: u64,
|
||||
}
|
||||
|
||||
impl PingPacket {
|
||||
pub fn new(payload: u64) -> Self {
|
||||
Self {
|
||||
packet_id: 1,
|
||||
payload,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl AsyncWriteToBuffer for PingPacket {
|
||||
async fn write_to_buffer(&self) -> Result<Vec<u8>, ProtocolError> {
|
||||
let mut buffer = Cursor::new(Vec::<u8>::new());
|
||||
|
||||
buffer.write_u64(self.payload).await?;
|
||||
|
||||
Ok(buffer.into_inner())
|
||||
}
|
||||
}
|
||||
|
||||
impl PacketId for PingPacket {
|
||||
fn get_packet_id(&self) -> usize {
|
||||
self.packet_id
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PongPacket {
|
||||
#[allow(dead_code)]
|
||||
pub packet_id: usize,
|
||||
pub payload: u64,
|
||||
}
|
||||
|
||||
impl ExpectedPacketId for PongPacket {
|
||||
fn get_expected_packet_id() -> usize {
|
||||
1
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl AsyncReadFromBuffer for PongPacket {
|
||||
async fn read_from_buffer(buffer: Vec<u8>) -> Result<Self, ProtocolError> {
|
||||
let mut reader = Cursor::new(buffer);
|
||||
|
||||
let payload = reader.read_u64().await?;
|
||||
|
||||
Ok(PongPacket {
|
||||
packet_id: 0,
|
||||
payload,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::io::Cursor;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_varint_roundtrip() {
|
||||
let test_cases = vec![
|
||||
0usize, 1, 127, 128, 255, 256, 16383, 16384, 2097151, 2097152, 268435455,
|
||||
];
|
||||
|
||||
for value in test_cases {
|
||||
let mut buffer = Cursor::new(Vec::new());
|
||||
buffer.write_varint(value).await.unwrap();
|
||||
|
||||
let mut reader = Cursor::new(buffer.into_inner());
|
||||
let result = reader.read_varint().await.unwrap();
|
||||
|
||||
assert_eq!(value, result, "Varint roundtrip failed for {}", value);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_varint_encoding() {
|
||||
// Test specific known encodings
|
||||
let cases = vec![
|
||||
(0usize, vec![0x00]),
|
||||
(1, vec![0x01]),
|
||||
(127, vec![0x7f]),
|
||||
(128, vec![0x80, 0x01]),
|
||||
(255, vec![0xff, 0x01]),
|
||||
(25565, vec![0xdd, 0xc7, 0x01]),
|
||||
(2097151, vec![0xff, 0xff, 0x7f]),
|
||||
];
|
||||
|
||||
for (value, expected) in cases {
|
||||
let mut buffer = Cursor::new(Vec::new());
|
||||
buffer.write_varint(value).await.unwrap();
|
||||
assert_eq!(
|
||||
buffer.into_inner(),
|
||||
expected,
|
||||
"Varint encoding failed for {}",
|
||||
value
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_string_roundtrip() {
|
||||
let test_cases = vec![
|
||||
"",
|
||||
"hello",
|
||||
"localhost",
|
||||
"mc.example.com",
|
||||
"こんにちは", // Unicode
|
||||
];
|
||||
|
||||
for s in test_cases {
|
||||
let mut buffer = Cursor::new(Vec::new());
|
||||
buffer.write_string(s).await.unwrap();
|
||||
|
||||
let mut reader = Cursor::new(buffer.into_inner());
|
||||
let result = reader.read_string().await.unwrap();
|
||||
|
||||
assert_eq!(s, result, "String roundtrip failed for {:?}", s);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handshake_packet_serialization() {
|
||||
let packet = HandshakePacket::new(578, "localhost".to_string(), 25565);
|
||||
let buffer = packet.write_to_buffer().await.unwrap();
|
||||
|
||||
// Verify the buffer contains expected data
|
||||
let mut reader = Cursor::new(buffer);
|
||||
|
||||
// Protocol version (578 as varint)
|
||||
let protocol = reader.read_varint().await.unwrap();
|
||||
assert_eq!(protocol, 578);
|
||||
|
||||
// Server address
|
||||
let address = reader.read_string().await.unwrap();
|
||||
assert_eq!(address, "localhost");
|
||||
|
||||
// Server port (big-endian u16)
|
||||
let port = reader.read_u16().await.unwrap();
|
||||
assert_eq!(port, 25565);
|
||||
|
||||
// Next state (1 for status)
|
||||
let state = reader.read_varint().await.unwrap();
|
||||
assert_eq!(state, 1);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_request_packet_serialization() {
|
||||
let packet = RequestPacket::new();
|
||||
let buffer = packet.write_to_buffer().await.unwrap();
|
||||
|
||||
// Request packet has no data
|
||||
assert!(buffer.is_empty());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_ping_packet_serialization() {
|
||||
let packet = PingPacket::new(12345678);
|
||||
let buffer = packet.write_to_buffer().await.unwrap();
|
||||
|
||||
// Ping packet contains a u64 payload (8 bytes, big-endian)
|
||||
assert_eq!(buffer.len(), 8);
|
||||
|
||||
let mut reader = Cursor::new(buffer);
|
||||
let payload = reader.read_u64().await.unwrap();
|
||||
assert_eq!(payload, 12345678);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_response_packet_deserialization() {
|
||||
// Create a buffer with a JSON string
|
||||
let json = r#"{"version":{"name":"1.20.4","protocol":765}}"#;
|
||||
let mut buffer = Cursor::new(Vec::new());
|
||||
buffer.write_string(json).await.unwrap();
|
||||
|
||||
let packet = ResponsePacket::read_from_buffer(buffer.into_inner())
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(packet.body, json);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_pong_packet_deserialization() {
|
||||
let payload: u64 = 987654321;
|
||||
let buffer = payload.to_be_bytes().to_vec();
|
||||
|
||||
let packet = PongPacket::read_from_buffer(buffer).await.unwrap();
|
||||
assert_eq!(packet.payload, payload);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_invalid_varint() {
|
||||
// A varint with more than 5 continuation bytes is invalid
|
||||
let invalid = vec![0x80, 0x80, 0x80, 0x80, 0x80, 0x80];
|
||||
let mut reader = Cursor::new(invalid);
|
||||
|
||||
let result = reader.read_varint().await;
|
||||
assert!(matches!(result, Err(ProtocolError::InvalidVarInt)));
|
||||
}
|
||||
}
|
||||
428
packages/async-minecraft-ping/src/server.rs
Normal file
428
packages/async-minecraft-ping/src/server.rs
Normal file
@@ -0,0 +1,428 @@
|
||||
//! This module defines a wrapper around Minecraft's
|
||||
//! [ServerListPing](https://wiki.vg/Server_List_Ping)
|
||||
|
||||
use std::time::Duration;
|
||||
|
||||
use serde::Deserialize;
|
||||
use thiserror::Error;
|
||||
use tokio::net::TcpStream;
|
||||
|
||||
use crate::protocol::{self, AsyncReadRawPacket, AsyncWriteRawPacket};
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum ServerError {
|
||||
#[error("error reading or writing data")]
|
||||
ProtocolError,
|
||||
|
||||
#[error("failed to connect to server")]
|
||||
FailedToConnect,
|
||||
|
||||
#[error("connection timed out")]
|
||||
ConnectionTimedOut,
|
||||
|
||||
#[error("invalid JSON response: \"{0}\"")]
|
||||
InvalidJson(String),
|
||||
|
||||
#[error("mismatched pong payload (expected \"{expected}\", got \"{actual}\")")]
|
||||
MismatchedPayload { expected: u64, actual: u64 },
|
||||
}
|
||||
|
||||
impl From<protocol::ProtocolError> for ServerError {
|
||||
fn from(_err: protocol::ProtocolError) -> Self {
|
||||
ServerError::ProtocolError
|
||||
}
|
||||
}
|
||||
|
||||
/// Contains information about the server version.
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct ServerVersion {
|
||||
/// The server's Minecraft version, i.e. "1.15.2".
|
||||
pub name: String,
|
||||
|
||||
/// The server's ServerListPing protocol version.
|
||||
pub protocol: i32,
|
||||
}
|
||||
|
||||
/// Contains information about a player.
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct ServerPlayer {
|
||||
/// The player's in-game name.
|
||||
pub name: String,
|
||||
|
||||
/// The player's UUID.
|
||||
pub id: String,
|
||||
}
|
||||
|
||||
/// Contains information about the currently online
|
||||
/// players.
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct ServerPlayers {
|
||||
/// The configured maximum number of players for the
|
||||
/// server.
|
||||
pub max: i32,
|
||||
|
||||
/// The number of players currently online.
|
||||
pub online: i32,
|
||||
|
||||
/// An optional list of player information for
|
||||
/// currently online players.
|
||||
pub sample: Option<Vec<ServerPlayer>>,
|
||||
}
|
||||
|
||||
/// Contains the server's MOTD.
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(untagged)]
|
||||
pub enum ServerDescription {
|
||||
Plain(String),
|
||||
Object { text: String },
|
||||
}
|
||||
|
||||
/// The decoded JSON response from a status query over
|
||||
/// ServerListPing.
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct StatusResponse {
|
||||
/// Information about the server's version.
|
||||
pub version: ServerVersion,
|
||||
|
||||
/// Information about currently online players.
|
||||
pub players: ServerPlayers,
|
||||
|
||||
/// Single-field struct containing the server's MOTD.
|
||||
pub description: ServerDescription,
|
||||
|
||||
/// Optional field containing a path to the server's
|
||||
/// favicon.
|
||||
pub favicon: Option<String>,
|
||||
}
|
||||
|
||||
const LATEST_PROTOCOL_VERSION: usize = 578;
|
||||
const DEFAULT_PORT: u16 = 25565;
|
||||
const DEFAULT_TIMEOUT: Duration = Duration::from_secs(2);
|
||||
|
||||
/// Builder for a Minecraft
|
||||
/// ServerListPing connection.
|
||||
pub struct ConnectionConfig {
|
||||
protocol_version: usize,
|
||||
address: String,
|
||||
port: u16,
|
||||
timeout: Duration,
|
||||
#[cfg(feature = "srv")]
|
||||
srv_lookup: bool,
|
||||
}
|
||||
|
||||
impl ConnectionConfig {
|
||||
/// Initiates the Minecraft server
|
||||
/// connection build process.
|
||||
pub fn build<T: Into<String>>(address: T) -> Self {
|
||||
ConnectionConfig {
|
||||
protocol_version: LATEST_PROTOCOL_VERSION,
|
||||
address: address.into(),
|
||||
port: DEFAULT_PORT,
|
||||
timeout: DEFAULT_TIMEOUT,
|
||||
#[cfg(feature = "srv")]
|
||||
srv_lookup: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets a specific
|
||||
/// protocol version for the connection to
|
||||
/// use. If not specified, the latest version
|
||||
/// will be used.
|
||||
pub fn with_protocol_version(mut self, protocol_version: usize) -> Self {
|
||||
self.protocol_version = protocol_version;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets a specific port for the
|
||||
/// connection to use. If not specified, the
|
||||
/// default port of 25565 will be used.
|
||||
pub fn with_port(mut self, port: u16) -> Self {
|
||||
self.port = port;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets a specific timeout for the
|
||||
/// connection to use. If not specified, the
|
||||
/// timeout defaults to two seconds.
|
||||
pub fn with_timeout(mut self, timeout: Duration) -> Self {
|
||||
self.timeout = timeout;
|
||||
self
|
||||
}
|
||||
|
||||
/// Enables SRV record lookup for the connection.
|
||||
///
|
||||
/// When enabled, the library will query DNS for an SRV record
|
||||
/// at `_minecraft._tcp.<address>`. If found, the target host
|
||||
/// and port from the SRV record will be used instead of the
|
||||
/// configured address and port.
|
||||
///
|
||||
/// This feature requires the `srv` feature to be enabled.
|
||||
#[cfg(feature = "srv")]
|
||||
pub fn with_srv_lookup(mut self) -> Self {
|
||||
self.srv_lookup = true;
|
||||
self
|
||||
}
|
||||
|
||||
/// Connects to the server and consumes the builder.
|
||||
pub async fn connect(self) -> Result<StatusConnection, ServerError> {
|
||||
let (address, port) = self.resolve_address().await;
|
||||
|
||||
let stream = tokio::time::timeout(
|
||||
self.timeout,
|
||||
TcpStream::connect(format!("{}:{}", address, port)),
|
||||
)
|
||||
.await
|
||||
.map_err(|_| ServerError::ConnectionTimedOut)?
|
||||
.map_err(|_| ServerError::FailedToConnect)?;
|
||||
|
||||
Ok(StatusConnection {
|
||||
stream,
|
||||
protocol_version: self.protocol_version,
|
||||
address,
|
||||
port,
|
||||
timeout: self.timeout,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(feature = "srv")]
|
||||
async fn resolve_address(&self) -> (String, u16) {
|
||||
if !self.srv_lookup {
|
||||
return (self.address.clone(), self.port);
|
||||
}
|
||||
|
||||
// Try to resolve SRV record, fall back to original address on any failure
|
||||
match self.lookup_srv().await {
|
||||
Some((host, port)) => (host, port),
|
||||
None => (self.address.clone(), self.port),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "srv"))]
|
||||
async fn resolve_address(&self) -> (String, u16) {
|
||||
(self.address.clone(), self.port)
|
||||
}
|
||||
|
||||
#[cfg(feature = "srv")]
|
||||
async fn lookup_srv(&self) -> Option<(String, u16)> {
|
||||
use hickory_resolver::TokioAsyncResolver;
|
||||
|
||||
let resolver = TokioAsyncResolver::tokio_from_system_conf().ok()?;
|
||||
let srv_name = format!("_minecraft._tcp.{}", self.address);
|
||||
|
||||
let lookup = tokio::time::timeout(self.timeout, resolver.srv_lookup(&srv_name))
|
||||
.await
|
||||
.ok()?
|
||||
.ok()?;
|
||||
|
||||
let record = lookup.iter().next()?;
|
||||
let target = record.target().to_string();
|
||||
// Remove trailing dot from DNS name
|
||||
let host = target.trim_end_matches('.').to_string();
|
||||
let port = record.port();
|
||||
|
||||
Some((host, port))
|
||||
}
|
||||
}
|
||||
|
||||
/// Convenience wrapper for easily connecting
|
||||
/// to a server on the default port with
|
||||
/// the latest protocol version.
|
||||
pub async fn connect(address: String) -> Result<StatusConnection, ServerError> {
|
||||
ConnectionConfig::build(address).connect().await
|
||||
}
|
||||
|
||||
/// Wraps a built connection
|
||||
pub struct StatusConnection {
|
||||
stream: TcpStream,
|
||||
protocol_version: usize,
|
||||
address: String,
|
||||
port: u16,
|
||||
timeout: Duration,
|
||||
}
|
||||
|
||||
impl StatusConnection {
|
||||
/// Sends and reads the packets for the
|
||||
/// ServerListPing status call.
|
||||
///
|
||||
/// Consumes the connection and returns a type
|
||||
/// that can only issue pings. The resulting
|
||||
/// status body is accessible via the `status`
|
||||
/// property on `PingConnection`.
|
||||
pub async fn status(mut self) -> Result<PingConnection, ServerError> {
|
||||
let handshake = protocol::HandshakePacket::new(
|
||||
self.protocol_version,
|
||||
self.address.to_string(),
|
||||
self.port,
|
||||
);
|
||||
|
||||
self.stream
|
||||
.write_packet_with_timeout(handshake, self.timeout)
|
||||
.await?;
|
||||
|
||||
self.stream
|
||||
.write_packet_with_timeout(protocol::RequestPacket::new(), self.timeout)
|
||||
.await?;
|
||||
|
||||
let response: protocol::ResponsePacket =
|
||||
self.stream.read_packet_with_timeout(self.timeout).await?;
|
||||
|
||||
let status: StatusResponse = serde_json::from_str(&response.body)
|
||||
.map_err(|_| ServerError::InvalidJson(response.body))?;
|
||||
|
||||
Ok(PingConnection {
|
||||
stream: self.stream,
|
||||
protocol_version: self.protocol_version,
|
||||
address: self.address,
|
||||
port: self.port,
|
||||
status,
|
||||
timeout: self.timeout,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Wraps a built connection
|
||||
///
|
||||
/// Constructed by calling `status()` on
|
||||
/// a `StatusConnection` struct.
|
||||
#[allow(dead_code)]
|
||||
pub struct PingConnection {
|
||||
stream: TcpStream,
|
||||
protocol_version: usize,
|
||||
address: String,
|
||||
port: u16,
|
||||
timeout: Duration,
|
||||
pub status: StatusResponse,
|
||||
}
|
||||
|
||||
impl PingConnection {
|
||||
/// Sends a ping to the Minecraft server with the
|
||||
/// provided payload and asserts that the returned
|
||||
/// payload is the same.
|
||||
///
|
||||
/// Server closes the connection after a ping call,
|
||||
/// so this method consumes the connection.
|
||||
pub async fn ping(mut self, payload: u64) -> Result<(), ServerError> {
|
||||
let ping = protocol::PingPacket::new(payload);
|
||||
|
||||
self.stream
|
||||
.write_packet_with_timeout(ping, self.timeout)
|
||||
.await?;
|
||||
|
||||
let pong: protocol::PongPacket = self.stream.read_packet_with_timeout(self.timeout).await?;
|
||||
|
||||
if pong.payload != payload {
|
||||
return Err(ServerError::MismatchedPayload {
|
||||
expected: payload,
|
||||
actual: pong.payload,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_server_description_plain() {
|
||||
let json = r#""A Minecraft Server""#;
|
||||
let desc: ServerDescription = serde_json::from_str(json).unwrap();
|
||||
assert!(matches!(desc, ServerDescription::Plain(s) if s == "A Minecraft Server"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_server_description_object() {
|
||||
let json = r#"{"text":"A Minecraft Server"}"#;
|
||||
let desc: ServerDescription = serde_json::from_str(json).unwrap();
|
||||
assert!(matches!(desc, ServerDescription::Object { text } if text == "A Minecraft Server"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_status_response_minimal() {
|
||||
let json = r#"{
|
||||
"version": {"name": "1.20.4", "protocol": 765},
|
||||
"players": {"max": 20, "online": 5},
|
||||
"description": "Welcome to the server"
|
||||
}"#;
|
||||
|
||||
let response: StatusResponse = serde_json::from_str(json).unwrap();
|
||||
assert_eq!(response.version.name, "1.20.4");
|
||||
assert_eq!(response.version.protocol, 765);
|
||||
assert_eq!(response.players.max, 20);
|
||||
assert_eq!(response.players.online, 5);
|
||||
assert!(response.players.sample.is_none());
|
||||
assert!(response.favicon.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_status_response_with_players() {
|
||||
let json = r#"{
|
||||
"version": {"name": "1.20.4", "protocol": 765},
|
||||
"players": {
|
||||
"max": 20,
|
||||
"online": 2,
|
||||
"sample": [
|
||||
{"name": "Player1", "id": "uuid-1"},
|
||||
{"name": "Player2", "id": "uuid-2"}
|
||||
]
|
||||
},
|
||||
"description": {"text": "Welcome"}
|
||||
}"#;
|
||||
|
||||
let response: StatusResponse = serde_json::from_str(json).unwrap();
|
||||
let sample = response.players.sample.unwrap();
|
||||
assert_eq!(sample.len(), 2);
|
||||
assert_eq!(sample[0].name, "Player1");
|
||||
assert_eq!(sample[1].name, "Player2");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_status_response_with_favicon() {
|
||||
let json = r#"{
|
||||
"version": {"name": "1.20.4", "protocol": 765},
|
||||
"players": {"max": 20, "online": 0},
|
||||
"description": "Test",
|
||||
"favicon": "data:image/png;base64,iVBORw0KGgo="
|
||||
}"#;
|
||||
|
||||
let response: StatusResponse = serde_json::from_str(json).unwrap();
|
||||
assert!(response.favicon.is_some());
|
||||
assert!(response.favicon.unwrap().starts_with("data:image/png"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_connection_config_defaults() {
|
||||
let config = ConnectionConfig::build("localhost");
|
||||
assert_eq!(config.address, "localhost");
|
||||
assert_eq!(config.port, DEFAULT_PORT);
|
||||
assert_eq!(config.timeout, DEFAULT_TIMEOUT);
|
||||
assert_eq!(config.protocol_version, LATEST_PROTOCOL_VERSION);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_connection_config_with_port() {
|
||||
let config = ConnectionConfig::build("localhost").with_port(12345);
|
||||
assert_eq!(config.port, 12345);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_connection_config_with_timeout() {
|
||||
let config = ConnectionConfig::build("localhost").with_timeout(Duration::from_secs(10));
|
||||
assert_eq!(config.timeout, Duration::from_secs(10));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_connection_config_with_protocol_version() {
|
||||
let config = ConnectionConfig::build("localhost").with_protocol_version(47);
|
||||
assert_eq!(config.protocol_version, 47);
|
||||
}
|
||||
|
||||
#[cfg(feature = "srv")]
|
||||
#[test]
|
||||
fn test_connection_config_with_srv_lookup() {
|
||||
let config = ConnectionConfig::build("localhost").with_srv_lookup();
|
||||
assert!(config.srv_lookup);
|
||||
}
|
||||
}
|
||||
320
packages/async-minecraft-ping/tests/integration.rs
Normal file
320
packages/async-minecraft-ping/tests/integration.rs
Normal file
@@ -0,0 +1,320 @@
|
||||
//! Integration tests using mocked TCP streams
|
||||
|
||||
use std::io::Cursor;
|
||||
use std::time::Duration;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt, DuplexStream};
|
||||
|
||||
// Re-implement the wire protocol traits for testing
|
||||
// since they're not exported from the library
|
||||
|
||||
#[async_trait]
|
||||
trait AsyncWireWriteExt {
|
||||
async fn write_varint(&mut self, int: usize) -> std::io::Result<()>;
|
||||
async fn write_string(&mut self, string: &str) -> std::io::Result<()>;
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<W: tokio::io::AsyncWrite + Unpin + Send + Sync> AsyncWireWriteExt for W {
|
||||
async fn write_varint(&mut self, int: usize) -> std::io::Result<()> {
|
||||
let mut int = (int as u64) & 0xFFFF_FFFF;
|
||||
let mut written = 0;
|
||||
let mut buffer = [0; 5];
|
||||
loop {
|
||||
let temp = (int & 0b0111_1111) as u8;
|
||||
int >>= 7;
|
||||
if int != 0 {
|
||||
buffer[written] = temp | 0b1000_0000;
|
||||
} else {
|
||||
buffer[written] = temp;
|
||||
}
|
||||
written += 1;
|
||||
if int == 0 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
self.write_all(&buffer[0..written]).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn write_string(&mut self, string: &str) -> std::io::Result<()> {
|
||||
self.write_varint(string.len()).await?;
|
||||
self.write_all(string.as_bytes()).await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[async_trait]
|
||||
trait AsyncWireReadExt {
|
||||
async fn read_varint(&mut self) -> std::io::Result<usize>;
|
||||
async fn read_string(&mut self) -> std::io::Result<String>;
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<R: tokio::io::AsyncRead + Unpin + Send + Sync> AsyncWireReadExt for R {
|
||||
async fn read_varint(&mut self) -> std::io::Result<usize> {
|
||||
let mut read = 0;
|
||||
let mut result = 0;
|
||||
loop {
|
||||
let mut buf = [0u8; 1];
|
||||
self.read_exact(&mut buf).await?;
|
||||
let read_value = buf[0];
|
||||
let value = read_value & 0b0111_1111;
|
||||
result |= (value as usize) << (7 * read);
|
||||
read += 1;
|
||||
if read > 5 {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidData,
|
||||
"invalid varint",
|
||||
));
|
||||
}
|
||||
if (read_value & 0b1000_0000) == 0 {
|
||||
return Ok(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn read_string(&mut self) -> std::io::Result<String> {
|
||||
let length = self.read_varint().await?;
|
||||
let mut buffer = vec![0; length];
|
||||
self.read_exact(&mut buffer).await?;
|
||||
String::from_utf8(buffer)
|
||||
.map_err(|_| std::io::Error::new(std::io::ErrorKind::InvalidData, "invalid utf8"))
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper to write a raw packet (length-prefixed with packet ID)
|
||||
async fn write_raw_packet(
|
||||
stream: &mut DuplexStream,
|
||||
packet_id: usize,
|
||||
data: &[u8],
|
||||
) -> std::io::Result<()> {
|
||||
let mut packet_buffer = Cursor::new(Vec::new());
|
||||
packet_buffer.write_varint(packet_id).await?;
|
||||
packet_buffer.write_all(data).await?;
|
||||
|
||||
let inner = packet_buffer.into_inner();
|
||||
stream.write_varint(inner.len()).await?;
|
||||
stream.write_all(&inner).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Helper to read a raw packet and return (packet_id, data)
|
||||
async fn read_raw_packet(stream: &mut DuplexStream) -> std::io::Result<(usize, Vec<u8>)> {
|
||||
let length = stream.read_varint().await?;
|
||||
if length == 0 {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidData,
|
||||
"empty packet",
|
||||
));
|
||||
}
|
||||
|
||||
let packet_id = stream.read_varint().await?;
|
||||
let mut data = vec![0u8; length - 1]; // -1 for the packet_id varint (assuming single byte)
|
||||
if !data.is_empty() {
|
||||
stream.read_exact(&mut data).await?;
|
||||
}
|
||||
|
||||
Ok((packet_id, data))
|
||||
}
|
||||
|
||||
/// Simulate a Minecraft server that responds to status requests
|
||||
async fn mock_server(mut stream: DuplexStream, response_json: &str) {
|
||||
// Read handshake packet (id=0)
|
||||
let (packet_id, _data) = read_raw_packet(&mut stream).await.unwrap();
|
||||
assert_eq!(packet_id, 0, "Expected handshake packet");
|
||||
|
||||
// Read request packet (id=0)
|
||||
let (packet_id, _data) = read_raw_packet(&mut stream).await.unwrap();
|
||||
assert_eq!(packet_id, 0, "Expected request packet");
|
||||
|
||||
// Send response packet (id=0) with JSON
|
||||
let mut response_data = Cursor::new(Vec::new());
|
||||
response_data.write_string(response_json).await.unwrap();
|
||||
write_raw_packet(&mut stream, 0, &response_data.into_inner())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Read ping packet (id=1)
|
||||
let (packet_id, data) = read_raw_packet(&mut stream).await.unwrap();
|
||||
assert_eq!(packet_id, 1, "Expected ping packet");
|
||||
|
||||
// Send pong packet (id=1) with same payload
|
||||
write_raw_packet(&mut stream, 1, &data).await.unwrap();
|
||||
}
|
||||
|
||||
/// Simulate a server that sends invalid JSON
|
||||
#[allow(dead_code)]
|
||||
async fn mock_server_invalid_json(mut stream: DuplexStream) {
|
||||
// Read handshake
|
||||
let _ = read_raw_packet(&mut stream).await.unwrap();
|
||||
// Read request
|
||||
let _ = read_raw_packet(&mut stream).await.unwrap();
|
||||
|
||||
// Send invalid JSON response
|
||||
let mut response_data = Cursor::new(Vec::new());
|
||||
response_data.write_string("not valid json").await.unwrap();
|
||||
write_raw_packet(&mut stream, 0, &response_data.into_inner())
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
/// Simulate a server that returns wrong pong payload
|
||||
#[allow(dead_code)]
|
||||
async fn mock_server_wrong_pong(mut stream: DuplexStream, response_json: &str) {
|
||||
// Read handshake
|
||||
let _ = read_raw_packet(&mut stream).await.unwrap();
|
||||
// Read request
|
||||
let _ = read_raw_packet(&mut stream).await.unwrap();
|
||||
|
||||
// Send valid response
|
||||
let mut response_data = Cursor::new(Vec::new());
|
||||
response_data.write_string(response_json).await.unwrap();
|
||||
write_raw_packet(&mut stream, 0, &response_data.into_inner())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Read ping
|
||||
let _ = read_raw_packet(&mut stream).await.unwrap();
|
||||
|
||||
// Send pong with different payload
|
||||
let wrong_payload: u64 = 99999;
|
||||
write_raw_packet(&mut stream, 1, &wrong_payload.to_be_bytes())
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
fn valid_status_json() -> &'static str {
|
||||
r#"{"version":{"name":"1.20.4","protocol":765},"players":{"max":20,"online":5},"description":"Test Server"}"#
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_mock_server_protocol() {
|
||||
// Test that our mock server correctly handles the Minecraft protocol
|
||||
let (mut client_stream, server_stream) = tokio::io::duplex(1024);
|
||||
|
||||
// Spawn mock server
|
||||
let server_handle = tokio::spawn(mock_server(server_stream, valid_status_json()));
|
||||
|
||||
// Simulate client sending handshake packet (id=0)
|
||||
let mut handshake_data = Cursor::new(Vec::new());
|
||||
handshake_data.write_varint(578).await.unwrap(); // protocol version
|
||||
handshake_data.write_string("localhost").await.unwrap(); // server address
|
||||
handshake_data.write_u16(25565).await.unwrap(); // port
|
||||
handshake_data.write_varint(1).await.unwrap(); // next state (status)
|
||||
write_raw_packet(&mut client_stream, 0, &handshake_data.into_inner())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Send request packet (id=0, empty)
|
||||
write_raw_packet(&mut client_stream, 0, &[]).await.unwrap();
|
||||
|
||||
// Read response packet
|
||||
let (packet_id, data) = read_raw_packet(&mut client_stream).await.unwrap();
|
||||
assert_eq!(packet_id, 0);
|
||||
|
||||
// Parse the JSON from response
|
||||
let mut cursor = Cursor::new(data);
|
||||
let json_str = cursor.read_string().await.unwrap();
|
||||
assert!(json_str.contains("1.20.4"));
|
||||
assert!(json_str.contains("Test Server"));
|
||||
|
||||
// Send ping packet (id=1)
|
||||
let ping_payload: u64 = 12345;
|
||||
write_raw_packet(&mut client_stream, 1, &ping_payload.to_be_bytes())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Read pong packet
|
||||
let (packet_id, data) = read_raw_packet(&mut client_stream).await.unwrap();
|
||||
assert_eq!(packet_id, 1);
|
||||
assert_eq!(data.len(), 8);
|
||||
let pong_payload = u64::from_be_bytes(data.try_into().unwrap());
|
||||
assert_eq!(pong_payload, ping_payload);
|
||||
|
||||
server_handle.await.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_status_json_parsing_plain_description() {
|
||||
use async_minecraft_ping::StatusResponse;
|
||||
|
||||
let json = r#"{
|
||||
"version": {"name": "1.20.4", "protocol": 765},
|
||||
"players": {"max": 100, "online": 42},
|
||||
"description": "Plain text MOTD"
|
||||
}"#;
|
||||
|
||||
let response: StatusResponse = serde_json::from_str(json).unwrap();
|
||||
assert_eq!(response.version.name, "1.20.4");
|
||||
assert_eq!(response.version.protocol, 765);
|
||||
assert_eq!(response.players.max, 100);
|
||||
assert_eq!(response.players.online, 42);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_status_json_parsing_object_description() {
|
||||
use async_minecraft_ping::StatusResponse;
|
||||
|
||||
let json = r#"{
|
||||
"version": {"name": "1.19.4", "protocol": 762},
|
||||
"players": {"max": 50, "online": 10, "sample": [{"name": "Notch", "id": "069a79f4-44e9-4726-a5be-fca90e38aaf5"}]},
|
||||
"description": {"text": "Object MOTD"},
|
||||
"favicon": "data:image/png;base64,abc123"
|
||||
}"#;
|
||||
|
||||
let response: StatusResponse = serde_json::from_str(json).unwrap();
|
||||
assert_eq!(response.version.name, "1.19.4");
|
||||
assert_eq!(response.players.online, 10);
|
||||
assert!(response.players.sample.is_some());
|
||||
assert_eq!(response.players.sample.as_ref().unwrap().len(), 1);
|
||||
assert_eq!(response.players.sample.as_ref().unwrap()[0].name, "Notch");
|
||||
assert!(response.favicon.is_some());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_connection_config_builder_chain() {
|
||||
use async_minecraft_ping::ConnectionConfig;
|
||||
|
||||
let _config = ConnectionConfig::build("mc.example.com")
|
||||
.with_port(25566)
|
||||
.with_protocol_version(47)
|
||||
.with_timeout(Duration::from_secs(5));
|
||||
|
||||
// We can't directly inspect private fields, but we can verify
|
||||
// the builder pattern works without panicking
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_connection_refused() {
|
||||
use async_minecraft_ping::ConnectionConfig;
|
||||
|
||||
// Try to connect to a port that's definitely not listening
|
||||
let result = ConnectionConfig::build("127.0.0.1")
|
||||
.with_port(1) // Port 1 is privileged and unlikely to have a server
|
||||
.with_timeout(Duration::from_millis(100))
|
||||
.connect()
|
||||
.await;
|
||||
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[cfg(feature = "srv")]
|
||||
#[tokio::test]
|
||||
async fn test_srv_lookup_fallback() {
|
||||
use async_minecraft_ping::ConnectionConfig;
|
||||
|
||||
// This domain definitely doesn't have an SRV record
|
||||
// The library should fall back to the original address
|
||||
let result = ConnectionConfig::build("127.0.0.1")
|
||||
.with_port(1)
|
||||
.with_timeout(Duration::from_millis(100))
|
||||
.with_srv_lookup()
|
||||
.connect()
|
||||
.await;
|
||||
|
||||
// Should fail to connect (no server), but shouldn't fail on SRV lookup
|
||||
assert!(result.is_err());
|
||||
}
|
||||
Reference in New Issue
Block a user