├── .github
└── workflows
│ └── build.yml
├── .gitignore
├── .idea
├── .gitignore
├── mc-query.iml
├── modules.xml
└── vcs.xml
├── .vscode
└── launch.json
├── Cargo.lock
├── Cargo.toml
├── LICENSE-APACHE
├── LICENSE-MIT
├── README.md
├── resources
└── server.properties
├── src
├── errors.rs
├── lib.rs
├── query.rs
├── rcon.rs
├── rcon
│ ├── client.rs
│ └── packet.rs
├── socket.rs
├── status.rs
├── status
│ ├── data.rs
│ └── packet.rs
└── varint.rs
└── test
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | on:
2 | - push
3 | - pull_request
4 |
5 | jobs:
6 | lint:
7 | runs-on: ubuntu-latest
8 |
9 | steps:
10 | - uses: actions/checkout@v3
11 |
12 | - uses: actions-rs/toolchain@v1
13 | with:
14 | toolchain: stable
15 | default: true
16 | override: true
17 |
18 | - run: cargo clippy -- --deny warnings
19 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /target
2 | /server
3 |
--------------------------------------------------------------------------------
/.idea/.gitignore:
--------------------------------------------------------------------------------
1 | # Default ignored files
2 | /shelf/
3 | /workspace.xml
4 | # Editor-based HTTP Client requests
5 | /httpRequests/
6 | # Datasource local storage ignored files
7 | /dataSources/
8 | /dataSources.local.xml
9 |
--------------------------------------------------------------------------------
/.idea/mc-query.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/.idea/modules.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | // Use IntelliSense to learn about possible attributes.
3 | // Hover to view descriptions of existing attributes.
4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5 | "version": "0.2.0",
6 | "configurations": [
7 | {
8 | "type": "lldb",
9 | "request": "launch",
10 | "name": "Debug unit tests in library 'mc-query'",
11 | "cargo": {
12 | "args": ["test", "--no-run", "--lib", "--package=mc-query"],
13 | "filter": {
14 | "name": "mc-query",
15 | "kind": "lib"
16 | }
17 | },
18 | "args": [],
19 | "cwd": "${workspaceFolder}"
20 | }
21 | ]
22 | }
23 |
--------------------------------------------------------------------------------
/Cargo.lock:
--------------------------------------------------------------------------------
1 | # This file is automatically @generated by Cargo.
2 | # It is not intended for manual editing.
3 | version = 4
4 |
5 | [[package]]
6 | name = "async-trait"
7 | version = "0.1.68"
8 | source = "registry+https://github.com/rust-lang/crates.io-index"
9 | checksum = "b9ccdd8f2a161be9bd5c023df56f1b2a0bd1d83872ae53b71a84a12c9bf6e842"
10 | dependencies = [
11 | "proc-macro2",
12 | "quote",
13 | "syn",
14 | ]
15 |
16 | [[package]]
17 | name = "autocfg"
18 | version = "1.1.0"
19 | source = "registry+https://github.com/rust-lang/crates.io-index"
20 | checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
21 |
22 | [[package]]
23 | name = "bitflags"
24 | version = "1.3.2"
25 | source = "registry+https://github.com/rust-lang/crates.io-index"
26 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
27 |
28 | [[package]]
29 | name = "bytes"
30 | version = "1.4.0"
31 | source = "registry+https://github.com/rust-lang/crates.io-index"
32 | checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be"
33 | dependencies = [
34 | "serde",
35 | ]
36 |
37 | [[package]]
38 | name = "cfg-if"
39 | version = "1.0.0"
40 | source = "registry+https://github.com/rust-lang/crates.io-index"
41 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
42 |
43 | [[package]]
44 | name = "getrandom"
45 | version = "0.2.9"
46 | source = "registry+https://github.com/rust-lang/crates.io-index"
47 | checksum = "c85e1d9ab2eadba7e5040d4e09cbd6d072b76a557ad64e797c2cb9d4da21d7e4"
48 | dependencies = [
49 | "cfg-if",
50 | "libc",
51 | "wasi",
52 | ]
53 |
54 | [[package]]
55 | name = "hermit-abi"
56 | version = "0.2.6"
57 | source = "registry+https://github.com/rust-lang/crates.io-index"
58 | checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7"
59 | dependencies = [
60 | "libc",
61 | ]
62 |
63 | [[package]]
64 | name = "itoa"
65 | version = "1.0.6"
66 | source = "registry+https://github.com/rust-lang/crates.io-index"
67 | checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6"
68 |
69 | [[package]]
70 | name = "libc"
71 | version = "0.2.141"
72 | source = "registry+https://github.com/rust-lang/crates.io-index"
73 | checksum = "3304a64d199bb964be99741b7a14d26972741915b3649639149b2479bb46f4b5"
74 |
75 | [[package]]
76 | name = "lock_api"
77 | version = "0.4.9"
78 | source = "registry+https://github.com/rust-lang/crates.io-index"
79 | checksum = "435011366fe56583b16cf956f9df0095b405b82d76425bc8981c0e22e60ec4df"
80 | dependencies = [
81 | "autocfg",
82 | "scopeguard",
83 | ]
84 |
85 | [[package]]
86 | name = "log"
87 | version = "0.4.17"
88 | source = "registry+https://github.com/rust-lang/crates.io-index"
89 | checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e"
90 | dependencies = [
91 | "cfg-if",
92 | ]
93 |
94 | [[package]]
95 | name = "mc-query"
96 | version = "2.0.0"
97 | dependencies = [
98 | "async-trait",
99 | "bytes",
100 | "paste",
101 | "rand",
102 | "serde",
103 | "serde_json",
104 | "thiserror",
105 | "tokio",
106 | ]
107 |
108 | [[package]]
109 | name = "mio"
110 | version = "0.8.6"
111 | source = "registry+https://github.com/rust-lang/crates.io-index"
112 | checksum = "5b9d9a46eff5b4ff64b45a9e316a6d1e0bc719ef429cbec4dc630684212bfdf9"
113 | dependencies = [
114 | "libc",
115 | "log",
116 | "wasi",
117 | "windows-sys",
118 | ]
119 |
120 | [[package]]
121 | name = "num_cpus"
122 | version = "1.15.0"
123 | source = "registry+https://github.com/rust-lang/crates.io-index"
124 | checksum = "0fac9e2da13b5eb447a6ce3d392f23a29d8694bff781bf03a16cd9ac8697593b"
125 | dependencies = [
126 | "hermit-abi",
127 | "libc",
128 | ]
129 |
130 | [[package]]
131 | name = "parking_lot"
132 | version = "0.12.1"
133 | source = "registry+https://github.com/rust-lang/crates.io-index"
134 | checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f"
135 | dependencies = [
136 | "lock_api",
137 | "parking_lot_core",
138 | ]
139 |
140 | [[package]]
141 | name = "parking_lot_core"
142 | version = "0.9.7"
143 | source = "registry+https://github.com/rust-lang/crates.io-index"
144 | checksum = "9069cbb9f99e3a5083476ccb29ceb1de18b9118cafa53e90c9551235de2b9521"
145 | dependencies = [
146 | "cfg-if",
147 | "libc",
148 | "redox_syscall",
149 | "smallvec",
150 | "windows-sys",
151 | ]
152 |
153 | [[package]]
154 | name = "paste"
155 | version = "1.0.15"
156 | source = "registry+https://github.com/rust-lang/crates.io-index"
157 | checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
158 |
159 | [[package]]
160 | name = "pin-project-lite"
161 | version = "0.2.9"
162 | source = "registry+https://github.com/rust-lang/crates.io-index"
163 | checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116"
164 |
165 | [[package]]
166 | name = "ppv-lite86"
167 | version = "0.2.17"
168 | source = "registry+https://github.com/rust-lang/crates.io-index"
169 | checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
170 |
171 | [[package]]
172 | name = "proc-macro2"
173 | version = "1.0.56"
174 | source = "registry+https://github.com/rust-lang/crates.io-index"
175 | checksum = "2b63bdb0cd06f1f4dedf69b254734f9b45af66e4a031e42a7480257d9898b435"
176 | dependencies = [
177 | "unicode-ident",
178 | ]
179 |
180 | [[package]]
181 | name = "quote"
182 | version = "1.0.26"
183 | source = "registry+https://github.com/rust-lang/crates.io-index"
184 | checksum = "4424af4bf778aae2051a77b60283332f386554255d722233d09fbfc7e30da2fc"
185 | dependencies = [
186 | "proc-macro2",
187 | ]
188 |
189 | [[package]]
190 | name = "rand"
191 | version = "0.8.5"
192 | source = "registry+https://github.com/rust-lang/crates.io-index"
193 | checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
194 | dependencies = [
195 | "libc",
196 | "rand_chacha",
197 | "rand_core",
198 | ]
199 |
200 | [[package]]
201 | name = "rand_chacha"
202 | version = "0.3.1"
203 | source = "registry+https://github.com/rust-lang/crates.io-index"
204 | checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
205 | dependencies = [
206 | "ppv-lite86",
207 | "rand_core",
208 | ]
209 |
210 | [[package]]
211 | name = "rand_core"
212 | version = "0.6.4"
213 | source = "registry+https://github.com/rust-lang/crates.io-index"
214 | checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
215 | dependencies = [
216 | "getrandom",
217 | ]
218 |
219 | [[package]]
220 | name = "redox_syscall"
221 | version = "0.2.16"
222 | source = "registry+https://github.com/rust-lang/crates.io-index"
223 | checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a"
224 | dependencies = [
225 | "bitflags",
226 | ]
227 |
228 | [[package]]
229 | name = "ryu"
230 | version = "1.0.13"
231 | source = "registry+https://github.com/rust-lang/crates.io-index"
232 | checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041"
233 |
234 | [[package]]
235 | name = "scopeguard"
236 | version = "1.1.0"
237 | source = "registry+https://github.com/rust-lang/crates.io-index"
238 | checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
239 |
240 | [[package]]
241 | name = "serde"
242 | version = "1.0.160"
243 | source = "registry+https://github.com/rust-lang/crates.io-index"
244 | checksum = "bb2f3770c8bce3bcda7e149193a069a0f4365bda1fa5cd88e03bca26afc1216c"
245 | dependencies = [
246 | "serde_derive",
247 | ]
248 |
249 | [[package]]
250 | name = "serde_derive"
251 | version = "1.0.160"
252 | source = "registry+https://github.com/rust-lang/crates.io-index"
253 | checksum = "291a097c63d8497e00160b166a967a4a79c64f3facdd01cbd7502231688d77df"
254 | dependencies = [
255 | "proc-macro2",
256 | "quote",
257 | "syn",
258 | ]
259 |
260 | [[package]]
261 | name = "serde_json"
262 | version = "1.0.96"
263 | source = "registry+https://github.com/rust-lang/crates.io-index"
264 | checksum = "057d394a50403bcac12672b2b18fb387ab6d289d957dab67dd201875391e52f1"
265 | dependencies = [
266 | "itoa",
267 | "ryu",
268 | "serde",
269 | ]
270 |
271 | [[package]]
272 | name = "signal-hook-registry"
273 | version = "1.4.1"
274 | source = "registry+https://github.com/rust-lang/crates.io-index"
275 | checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1"
276 | dependencies = [
277 | "libc",
278 | ]
279 |
280 | [[package]]
281 | name = "smallvec"
282 | version = "1.10.0"
283 | source = "registry+https://github.com/rust-lang/crates.io-index"
284 | checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0"
285 |
286 | [[package]]
287 | name = "socket2"
288 | version = "0.4.9"
289 | source = "registry+https://github.com/rust-lang/crates.io-index"
290 | checksum = "64a4a911eed85daf18834cfaa86a79b7d266ff93ff5ba14005426219480ed662"
291 | dependencies = [
292 | "libc",
293 | "winapi",
294 | ]
295 |
296 | [[package]]
297 | name = "syn"
298 | version = "2.0.15"
299 | source = "registry+https://github.com/rust-lang/crates.io-index"
300 | checksum = "a34fcf3e8b60f57e6a14301a2e916d323af98b0ea63c599441eec8558660c822"
301 | dependencies = [
302 | "proc-macro2",
303 | "quote",
304 | "unicode-ident",
305 | ]
306 |
307 | [[package]]
308 | name = "thiserror"
309 | version = "1.0.40"
310 | source = "registry+https://github.com/rust-lang/crates.io-index"
311 | checksum = "978c9a314bd8dc99be594bc3c175faaa9794be04a5a5e153caba6915336cebac"
312 | dependencies = [
313 | "thiserror-impl",
314 | ]
315 |
316 | [[package]]
317 | name = "thiserror-impl"
318 | version = "1.0.40"
319 | source = "registry+https://github.com/rust-lang/crates.io-index"
320 | checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f"
321 | dependencies = [
322 | "proc-macro2",
323 | "quote",
324 | "syn",
325 | ]
326 |
327 | [[package]]
328 | name = "tokio"
329 | version = "1.27.0"
330 | source = "registry+https://github.com/rust-lang/crates.io-index"
331 | checksum = "d0de47a4eecbe11f498978a9b29d792f0d2692d1dd003650c24c76510e3bc001"
332 | dependencies = [
333 | "autocfg",
334 | "bytes",
335 | "libc",
336 | "mio",
337 | "num_cpus",
338 | "parking_lot",
339 | "pin-project-lite",
340 | "signal-hook-registry",
341 | "socket2",
342 | "tokio-macros",
343 | "windows-sys",
344 | ]
345 |
346 | [[package]]
347 | name = "tokio-macros"
348 | version = "2.0.0"
349 | source = "registry+https://github.com/rust-lang/crates.io-index"
350 | checksum = "61a573bdc87985e9d6ddeed1b3d864e8a302c847e40d647746df2f1de209d1ce"
351 | dependencies = [
352 | "proc-macro2",
353 | "quote",
354 | "syn",
355 | ]
356 |
357 | [[package]]
358 | name = "unicode-ident"
359 | version = "1.0.8"
360 | source = "registry+https://github.com/rust-lang/crates.io-index"
361 | checksum = "e5464a87b239f13a63a501f2701565754bae92d243d4bb7eb12f6d57d2269bf4"
362 |
363 | [[package]]
364 | name = "wasi"
365 | version = "0.11.0+wasi-snapshot-preview1"
366 | source = "registry+https://github.com/rust-lang/crates.io-index"
367 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
368 |
369 | [[package]]
370 | name = "winapi"
371 | version = "0.3.9"
372 | source = "registry+https://github.com/rust-lang/crates.io-index"
373 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
374 | dependencies = [
375 | "winapi-i686-pc-windows-gnu",
376 | "winapi-x86_64-pc-windows-gnu",
377 | ]
378 |
379 | [[package]]
380 | name = "winapi-i686-pc-windows-gnu"
381 | version = "0.4.0"
382 | source = "registry+https://github.com/rust-lang/crates.io-index"
383 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
384 |
385 | [[package]]
386 | name = "winapi-x86_64-pc-windows-gnu"
387 | version = "0.4.0"
388 | source = "registry+https://github.com/rust-lang/crates.io-index"
389 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
390 |
391 | [[package]]
392 | name = "windows-sys"
393 | version = "0.45.0"
394 | source = "registry+https://github.com/rust-lang/crates.io-index"
395 | checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0"
396 | dependencies = [
397 | "windows-targets",
398 | ]
399 |
400 | [[package]]
401 | name = "windows-targets"
402 | version = "0.42.2"
403 | source = "registry+https://github.com/rust-lang/crates.io-index"
404 | checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071"
405 | dependencies = [
406 | "windows_aarch64_gnullvm",
407 | "windows_aarch64_msvc",
408 | "windows_i686_gnu",
409 | "windows_i686_msvc",
410 | "windows_x86_64_gnu",
411 | "windows_x86_64_gnullvm",
412 | "windows_x86_64_msvc",
413 | ]
414 |
415 | [[package]]
416 | name = "windows_aarch64_gnullvm"
417 | version = "0.42.2"
418 | source = "registry+https://github.com/rust-lang/crates.io-index"
419 | checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8"
420 |
421 | [[package]]
422 | name = "windows_aarch64_msvc"
423 | version = "0.42.2"
424 | source = "registry+https://github.com/rust-lang/crates.io-index"
425 | checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43"
426 |
427 | [[package]]
428 | name = "windows_i686_gnu"
429 | version = "0.42.2"
430 | source = "registry+https://github.com/rust-lang/crates.io-index"
431 | checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f"
432 |
433 | [[package]]
434 | name = "windows_i686_msvc"
435 | version = "0.42.2"
436 | source = "registry+https://github.com/rust-lang/crates.io-index"
437 | checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060"
438 |
439 | [[package]]
440 | name = "windows_x86_64_gnu"
441 | version = "0.42.2"
442 | source = "registry+https://github.com/rust-lang/crates.io-index"
443 | checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36"
444 |
445 | [[package]]
446 | name = "windows_x86_64_gnullvm"
447 | version = "0.42.2"
448 | source = "registry+https://github.com/rust-lang/crates.io-index"
449 | checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3"
450 |
451 | [[package]]
452 | name = "windows_x86_64_msvc"
453 | version = "0.42.2"
454 | source = "registry+https://github.com/rust-lang/crates.io-index"
455 | checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0"
456 |
--------------------------------------------------------------------------------
/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "mc-query"
3 | version = "2.0.0"
4 | edition = "2021"
5 | authors = ["Ari Prakash"]
6 | description = "Implementations of Server List Ping, Query, and RCON for minecraft servers"
7 | documentation = "https://docs.rs/mc-query"
8 | readme = "README.md"
9 | homepage = "https://github.com/ariscript/mc-query"
10 | repository = "https://github.com/ariscript/mc-query"
11 | license = "MIT OR Apache-2.0"
12 | keywords = ["minecraft", "rcon", "query"]
13 | categories = ["api-bindings", "network-programming"]
14 | exclude = ["/test", "/resources"]
15 |
16 | [dependencies]
17 | async-trait = "0.1.68"
18 | bytes = { version = "1.4.0", features = ["serde"] }
19 | paste = "1.0.15"
20 | rand = "0.8.5"
21 | serde = { version = "1.0.160", features = ["derive"] }
22 | serde_json = "1.0.96"
23 | thiserror = "1.0.40"
24 | tokio = { version = "1.27.0", features = ["full"] }
25 |
--------------------------------------------------------------------------------
/LICENSE-APACHE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright 2022 Ari Prakash
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/LICENSE-MIT:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 Ari Prakash
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # mc-query
2 |
3 | 
4 | 
5 | 
6 |
7 | 
8 |
9 | Implementations of [Server List ping](https://wiki.vg/Server_List_Ping), [Query](https://wiki.vg/Query), and [RCON](https://wiki.vg/RCON) using the Minecraft networking protocol.
10 |
11 | Maybe in the future there will be a CLI to access these features as well.
12 |
13 | ## Installation
14 |
15 | To use this library, just run `cargo add mc-query`.
16 |
17 | ## Usage
18 |
19 | You can read the docs [here](https://docs.rs/mc-query).
20 |
21 | ## Examples
22 |
23 | ### Using `status` to get basic server information
24 |
25 | ```rs
26 | use mc_query::status;
27 | use tokio::io::Result;
28 |
29 | #[tokio::main]
30 | async fn main() -> Result<()> {
31 | let data = status("mc.hypixel.net", 25565).await?;
32 | println!("{data:#?}");
33 |
34 | Ok(())
35 | }
36 | ```
37 |
38 | ### Using `RconClient` to run commands via RCON
39 |
40 | ```rs
41 | use mc_query::rcon::RconClient;
42 | use tokio::io::Result;
43 |
44 | #[tokio::main]
45 | async fn main() -> Result<()> {
46 | let mut client = RconClient::new("localhost", 25565);
47 | client.authenticate("supersecretrconpassword").await?;
48 |
49 | let response = client.run_command("time set 0").await?;
50 | println!("{response}");
51 |
52 | Ok(())
53 | }
54 | ```
55 |
56 | ### Using `stat_basic` to query the server
57 |
58 | ```rs
59 | use mc_query::query;
60 | use tokio;:io::Result;
61 |
62 | #[tokio::main]
63 | async fn main() -> Result<()> {
64 | let res = stat_basic("localhost", 25565).await?;
65 | println!(
66 | "Server has {} out of {} players online",
67 | res.num_players,
68 | res.max_players
69 | );
70 |
71 | Ok(())
72 | }
73 | ```
74 |
75 | ### Using `stat_full` to query the server
76 |
77 | ```rs
78 | use mc_query::query;
79 | use tokio;:io::Result;
80 |
81 | #[tokio::main]
82 | async fn main() -> Result<()> {
83 | let res = stat_full("localhost", 25565).await?;
84 | println!("Online players: {:#?}, res.players);
85 |
86 | Ok(())
87 | }
88 | ```
89 |
90 | ## Reference
91 |
92 | - [wiki.vg](https://wiki.vg) - documentation of the various protocols implemented in this crate
93 |
94 | ## Testing
95 |
96 | Some tests in this library require a minecraft server to be running on `localhost`.
97 | If you are contributing a feature or bugfix that involves one of these tests,
98 | run the convienient testing script `./test` (or `py -3 test` on Windows).
99 | You can also just run a minecraft server without the cargo tests (useful for debugging with IDEs) with `./test --server-only true`.
100 |
101 | This requires a decently modern version of Python 3, and Java 17 or higher to run the server.
102 |
103 | ## License
104 |
105 | Licensed under either of
106 |
107 | - Apache License, Version 2.0
108 | ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0)
109 | - MIT license
110 | ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT)
111 |
112 | at your option.
113 |
114 | ## Contribution
115 |
116 | Unless you explicitly state otherwise, any contribution intentionally submitted
117 | for inclusion in the work by you, as defined in the Apache-2.0 license, shall be
118 | dual licensed as above, without any additional terms or conditions.
119 |
120 | ## Mojang
121 |
122 | This project is in no way involved with or endorsed by Mojang Synergies AB or Microsoft Corporation.
123 | Any use of their services (including running some tests in this library) requires you to agree to their [terms](https://minecraft.net/eula).
124 |
--------------------------------------------------------------------------------
/resources/server.properties:
--------------------------------------------------------------------------------
1 | #Minecraft server properties
2 | #Fri Jul 29 21:53:53 EDT 2022
3 | allow-flight=false
4 | allow-nether=true
5 | broadcast-console-to-ops=true
6 | broadcast-rcon-to-ops=true
7 | difficulty=easy
8 | enable-command-block=false
9 | enable-jmx-monitoring=false
10 | enable-query=true
11 | enable-rcon=true
12 | enable-status=true
13 | enforce-secure-profile=true
14 | enforce-whitelist=false
15 | entity-broadcast-range-percentage=100
16 | force-gamemode=false
17 | function-permission-level=2
18 | gamemode=survival
19 | generate-structures=true
20 | generator-settings={}
21 | hardcore=false
22 | hide-online-players=false
23 | level-name=world
24 | level-seed=
25 | level-type=minecraft\:normal
26 | max-chained-neighbor-updates=1000000
27 | max-players=20
28 | max-tick-time=60000
29 | max-world-size=29999984
30 | motd=A Minecraft Server
31 | network-compression-threshold=256
32 | online-mode=true
33 | op-permission-level=4
34 | player-idle-timeout=0
35 | prevent-proxy-connections=false
36 | previews-chat=false
37 | pvp=true
38 | query.port=25565
39 | rate-limit=0
40 | rcon.password=mc-query-test
41 | rcon.port=25575
42 | require-resource-pack=false
43 | resource-pack=
44 | resource-pack-prompt=
45 | resource-pack-sha1=
46 | server-ip=
47 | server-port=25565
48 | simulation-distance=10
49 | spawn-animals=true
50 | spawn-monsters=true
51 | spawn-npcs=true
52 | spawn-protection=16
53 | sync-chunk-writes=true
54 | text-filtering-config=
55 | use-native-transport=true
56 | view-distance=10
57 | white-list=false
58 |
--------------------------------------------------------------------------------
/src/errors.rs:
--------------------------------------------------------------------------------
1 | //! All the errors defined by this crate.
2 |
3 | use std::io::{self, ErrorKind};
4 | use thiserror::Error;
5 |
6 | /// An error from the Minecraft networking protocol.
7 | #[derive(Error, Debug)]
8 | pub enum MinecraftProtocolError {
9 | /// `VarInt` data was invalid according to the spec.
10 | #[error("invalid varint data")]
11 | InvalidVarInt,
12 |
13 | /// Received invalid state information from the server.
14 | #[error("invalid state")]
15 | InvalidState,
16 |
17 | /// Received incorrectly formatted status response from the server.
18 | #[error("invalid status response")]
19 | InvalidStatusResponse,
20 | }
21 |
22 | impl From for io::Error {
23 | fn from(err: MinecraftProtocolError) -> Self {
24 | io::Error::new(ErrorKind::InvalidData, err)
25 | }
26 | }
27 |
28 | /// An error from the RCON protocol.
29 | #[derive(Error, Debug)]
30 | pub enum RconProtocolError {
31 | /// Received non-ASCII payload data from the server.
32 | ///
33 | /// Note: some servers (for example Craftbukkit for Minecraft 1.4.7) reply
34 | /// with the section sign (0xa7) as a prefix for the payload. This error
35 | /// will not be returned in that case.
36 | #[error("non-ascii payload")]
37 | NonAsciiPayload,
38 |
39 | /// Authentication failed. You probably entered the wrong RCON password.
40 | #[error("authentication failed")]
41 | AuthFailed,
42 |
43 | /// Invalid or unexpected packet type received from the server.
44 | #[error("invalid packet type")]
45 | InvalidPacketType,
46 |
47 | /// Other kind of invalid response as defined by the spec.
48 | #[error("invalid rcon response")]
49 | InvalidRconResponse,
50 |
51 | /// Payload too long.
52 | ///
53 | /// | Direction | Payload Length limit |
54 | /// | ----------- | -------------------- |
55 | /// | Serverbound | 1446 |
56 | /// | Clientbound | 4096 |
57 | #[error("payload too long")]
58 | PayloadTooLong,
59 |
60 | /// Mismatch with the given request ID.
61 | ///
62 | /// Note: the server replies with a request ID of -1 in the case of an
63 | /// authentication failure. In that case, `AuthFailed` will be returned.
64 | /// This variant is returned if any *other* request ID was received.
65 | #[error("request id mismatch")]
66 | RequestIdMismatch,
67 | }
68 |
69 | impl From for io::Error {
70 | fn from(err: RconProtocolError) -> Self {
71 | io::Error::new(ErrorKind::InvalidData, err)
72 | }
73 | }
74 |
75 | /// An error from the Query protocol.
76 | #[derive(Error, Debug)]
77 | pub enum QueryProtocolError {
78 | /// Received invalid packet type.
79 | /// Valid types are 9 for handshake, 0 for stat
80 | #[error("invalid packet type")]
81 | InvalidPacketType,
82 |
83 | /// Unexpected packet type.
84 | #[error("unexpected packet type")]
85 | UnexpectedPacketType,
86 |
87 | /// Mismatch with the generated session ID.
88 | #[error("session id mismatch")]
89 | SessionIdMismatch,
90 |
91 | /// Received invalid challenge token from server.
92 | #[error("invalid challenge token")]
93 | InvalidChallengeToken,
94 |
95 | /// Invalid integer.
96 | /// Did not receive valid characters to parse as an integer in the string
97 | #[error("cannot parse int")]
98 | CannotParseInt,
99 |
100 | /// Invalid UTF8.
101 | /// Did not receive valid UTF from the server when a string was expected
102 | #[error("invalid UTF-8")]
103 | InvalidUtf8,
104 |
105 | /// Invalid key/value section.
106 | /// Expecting something like [this](https://wiki.vg/Query#K.2C_V_section)
107 | #[error("invalid key/value section")]
108 | InvalidKeyValueSection,
109 | }
110 |
111 | impl From for io::Error {
112 | fn from(err: QueryProtocolError) -> Self {
113 | io::Error::new(ErrorKind::InvalidData, err)
114 | }
115 | }
116 |
117 | pub(crate) fn timeout_err() -> io::Result {
118 | Err(io::Error::new(ErrorKind::TimedOut, "connection timed out"))
119 | }
120 |
--------------------------------------------------------------------------------
/src/lib.rs:
--------------------------------------------------------------------------------
1 | //! Implementations of [Server List ping](https://wiki.vg/Server_List_Ping),
2 | //! [Query](https://wiki.vg/Query), and [RCON](https://wiki.vg/RCON) using the
3 | //! Minecraft networking protocol.
4 |
5 | #![warn(missing_docs)]
6 | #![warn(clippy::pedantic)]
7 | #![allow(clippy::cast_possible_truncation)]
8 | #![allow(clippy::cast_possible_wrap)]
9 | #![allow(clippy::cast_sign_loss)]
10 | #![allow(clippy::cast_lossless)]
11 |
12 | macro_rules! create_timeout {
13 | ($name:ident, $ret:ty) => {
14 | ::paste::paste! {
15 | #[doc = concat!("Similar to [`", stringify!($name), "`]")]
16 | /// but with an added argument for timeout.
17 | ///
18 | /// Note that timeouts are not precise, and may vary on the order
19 | /// of milliseconds, because of the way the async event loop works.
20 | ///
21 | /// # Arguments
22 | /// * `host` - A string slice that holds the hostname of the server to connect to.
23 | /// * `port` - The port to connect to on that server.
24 | ///
25 | /// # Errors
26 | /// Returns `Err` on any condition that
27 | #[doc = concat!("[`", stringify!($name), "`]")]
28 | /// does, and also when the response is not fully recieved within `dur`.
29 | pub async fn [<$name _with_timeout>](
30 | host: &str,
31 | port: u16,
32 | dur: ::std::time::Duration,
33 | ) -> ::std::io::Result<$ret> {
34 | use crate::errors::timeout_err;
35 | use ::tokio::time::timeout;
36 |
37 | timeout(dur, $name(host, port))
38 | .await
39 | .unwrap_or(timeout_err::<$ret>())
40 | }
41 | }
42 | };
43 | }
44 |
45 | pub mod errors;
46 | pub mod query;
47 | pub mod rcon;
48 | mod socket;
49 | pub mod status;
50 | mod varint;
51 |
52 | pub use status::status;
53 |
--------------------------------------------------------------------------------
/src/query.rs:
--------------------------------------------------------------------------------
1 | //! Implementation of the [Query](https://wiki.vg/Query) protocol.
2 |
3 | use bytes::{Buf, BufMut, Bytes, BytesMut};
4 | use rand::random;
5 | use std::collections::HashMap;
6 | use std::time::Duration;
7 | use tokio::io;
8 | use tokio::net::UdpSocket;
9 | use tokio::time::timeout;
10 |
11 | use crate::errors::QueryProtocolError;
12 |
13 | const QUERY_MAGIC: u16 = 0xfe_fd;
14 | const SESSION_ID_MASK: u32 = 0x0f_0f_0f_0f;
15 |
16 | /// A response from the server's basic query.
17 | /// Taken from [wiki.vg](https://wiki.vg/Query#Response_2)
18 | #[derive(Debug)]
19 | pub struct BasicStatResponse {
20 | /// The "motd" - message shown in the server list by the client.
21 | pub motd: String,
22 |
23 | /// The server's game type.
24 | /// Vanilla servers hardcode this to "SMP".
25 | pub game_type: String,
26 |
27 | /// The server's world/map name.
28 | pub map: String,
29 |
30 | /// The current number of online players.
31 | pub num_players: usize,
32 |
33 | /// Maximum players online this server allows.
34 | pub max_players: usize,
35 |
36 | /// The port the serer is running on.
37 | pub host_port: u16,
38 |
39 | /// The server's IP address.
40 | pub host_ip: String,
41 | }
42 |
43 | /// A response from the server's full query.
44 | /// Taken from [wiki.vg](https://wiki.vg/Query#Response_3)
45 | #[derive(Debug)]
46 | pub struct FullStatResponse {
47 | /// The "motd" - message shown in the server list by the client.
48 | pub motd: String,
49 |
50 | /// The server's game type.
51 | /// Vanilla servers hardcode this to "SMP".
52 | pub game_type: String,
53 |
54 | /// The server's game ID.
55 | /// Vanilla servers hardcode this to "MINECRAFT".
56 | pub game_id: String,
57 |
58 | /// The server's game version.
59 | pub version: String,
60 |
61 | /// The plugins the server has installed.
62 | /// Vanilla servers return an empty string.
63 | /// Other server platforms may have their own format for this field.
64 | pub plugins: String,
65 |
66 | /// The server's world/map name.
67 | pub map: String,
68 |
69 | /// The current number of online players.
70 | pub num_players: usize,
71 |
72 | /// Maximum players online this server allows.
73 | pub max_players: usize,
74 |
75 | /// The port the server is running on.
76 | pub host_port: u16,
77 |
78 | /// The server's IP address.
79 | pub host_ip: String,
80 |
81 | /// The current list of online players.
82 | pub players: Vec,
83 | }
84 |
85 | async fn stat_send(sock: &UdpSocket, bytes: &[u8]) -> io::Result {
86 | sock.send(bytes).await?;
87 | Box::pin(timeout(Duration::from_millis(250), recv_packet(sock))).await?
88 | }
89 |
90 | /// Perform a basic stat query of the server per the [Query Protocol](https://wiki.vg/Query#Basic_Stat).
91 | /// Note that the server must have `query-enabled=true` set in its properties to get a response.
92 | /// The `query.port` property might also be different from `server.port`.
93 | ///
94 | /// # Arguments
95 | /// * `host` - the hostname/IP of thr server to query
96 | /// * `port` - the port that the server's Query is running on
97 | ///
98 | /// # Errors
99 | /// Will return `Err` if there was a network error, if the challenge token wasn't obtainable, or if
100 | /// invalid data was recieved.
101 | ///
102 | /// # Examples
103 | /// ```
104 | /// use mc_query::query;
105 | /// use tokio::io::Result;
106 | ///
107 | /// #[tokio::main]
108 | /// async fn main() -> Result<()> {
109 | /// let res = query::stat_basic("localhost", 25565).await?;
110 | /// println!("The server has {} players online out of {}", res.num_players, res.num_players);
111 | ///
112 | /// Ok(())
113 | /// }
114 | /// ```
115 | pub async fn stat_basic(host: &str, port: u16) -> io::Result {
116 | let socket = UdpSocket::bind("0.0.0.0:0").await?;
117 | socket.connect(format!("{host}:{port}")).await?;
118 |
119 | let (token, session) = Box::pin(handshake(&socket)).await?;
120 |
121 | let mut bytes = BytesMut::new();
122 | bytes.put_u16(QUERY_MAGIC);
123 | bytes.put_u8(0); // packet type 0 - stat
124 | bytes.put_i32(session);
125 | bytes.put_i32(token);
126 |
127 | let mut res = match stat_send(&socket, &bytes).await {
128 | Ok(v) => v,
129 | Err(_) => stat_send(&socket, &bytes).await?,
130 | };
131 |
132 | validate_packet(&mut res, 0, session)?;
133 |
134 | let motd = get_string(&mut res)?;
135 | let game_type = get_string(&mut res)?;
136 | let map = get_string(&mut res)?;
137 | let num_players = get_string(&mut res)?
138 | .parse()
139 | .map_err::(|_| QueryProtocolError::CannotParseInt.into())?;
140 | let max_players = get_string(&mut res)?
141 | .parse()
142 | .map_err::(|_| QueryProtocolError::CannotParseInt.into())?;
143 |
144 | let host_port = res.get_u16_le(); // shorts are little endian per protocol
145 |
146 | let host_ip = get_string(&mut res)?;
147 |
148 | Ok(BasicStatResponse {
149 | motd,
150 | game_type,
151 | map,
152 | num_players,
153 | max_players,
154 | host_port,
155 | host_ip,
156 | })
157 | }
158 |
159 | /// Perform a full stat query of the server per the [Query Protocol](https://wiki.vg/Query#Full_stat).
160 | /// Note that the server must have `query-enabled=true` set in its properties to get a response.
161 | /// The `query.port` property might also be different from `server.port`.
162 | ///
163 | /// # Arguments
164 | /// * `host` - the hostname/IP of thr server to query
165 | /// * `port` - the port that the server's Query is running on
166 | ///
167 | /// # Errors
168 | /// Will return `Err` if there was a network error, if the challenge token wasn't obtainable, or
169 | /// if invalid data was recieved.
170 | ///
171 | /// # Examples
172 | /// ```
173 | /// use mc_query::query;
174 | /// use tokio::io::Result;
175 | ///
176 | /// #[tokio::main]
177 | /// async fn main() -> Result<()> {
178 | /// let res = query::stat_full("localhost", 25565).await?;
179 | /// println!("The server has {} players online out of {}", res.num_players, res.num_players);
180 | ///
181 | /// Ok(())
182 | /// }
183 | /// ```
184 | pub async fn stat_full(host: &str, port: u16) -> io::Result {
185 | let socket = UdpSocket::bind("0.0.0.0:0").await?;
186 | socket.connect(format!("{host}:{port}")).await?;
187 |
188 | let (token, session) = Box::pin(handshake(&socket)).await?;
189 |
190 | let mut bytes = BytesMut::new();
191 | bytes.put_u16(QUERY_MAGIC);
192 | bytes.put_u8(0); // packet type 0 - stat
193 | bytes.put_i32(session);
194 | bytes.put_i32(token);
195 | bytes.put_u32(0); // 4 extra bytes required for full stat vs. basic
196 |
197 | let mut res = match stat_send(&socket, &bytes).await {
198 | Ok(v) => v,
199 | Err(_) => stat_send(&socket, &bytes).await?,
200 | };
201 |
202 | validate_packet(&mut res, 0, session)?;
203 |
204 | // skip 11 meaningless padding bytes
205 | res.advance(11);
206 |
207 | // K,V section
208 | let mut kv = HashMap::new();
209 | loop {
210 | let key = get_string(&mut res)?;
211 | if key.is_empty() {
212 | break;
213 | }
214 | let value = get_string(&mut res)?;
215 | kv.insert(key, value);
216 | }
217 |
218 | // excuse this horrendous code, I don't know of a better way
219 | let motd = kv
220 | .remove("hostname")
221 | .ok_or(QueryProtocolError::InvalidKeyValueSection)?;
222 | let game_type = kv
223 | .remove("gametype")
224 | .ok_or(QueryProtocolError::InvalidKeyValueSection)?;
225 | let game_id = kv
226 | .remove("game_id")
227 | .ok_or(QueryProtocolError::InvalidKeyValueSection)?;
228 | let version = kv
229 | .remove("version")
230 | .ok_or(QueryProtocolError::InvalidKeyValueSection)?;
231 | let plugins = kv
232 | .remove("plugins")
233 | .ok_or(QueryProtocolError::InvalidKeyValueSection)?;
234 | let map = kv
235 | .remove("map")
236 | .ok_or(QueryProtocolError::InvalidKeyValueSection)?;
237 | let num_players = kv
238 | .remove("numplayers")
239 | .ok_or(QueryProtocolError::InvalidKeyValueSection)?
240 | .parse()
241 | .map_err(|_| QueryProtocolError::CannotParseInt)?;
242 | let max_players = kv
243 | .remove("maxplayers")
244 | .ok_or(QueryProtocolError::InvalidKeyValueSection)?
245 | .parse()
246 | .map_err(|_| QueryProtocolError::CannotParseInt)?;
247 | let host_port = kv
248 | .remove("hostport")
249 | .ok_or(QueryProtocolError::InvalidKeyValueSection)?
250 | .parse()
251 | .map_err(|_| QueryProtocolError::CannotParseInt)?;
252 | let host_ip = kv
253 | .remove("hostip")
254 | .ok_or(QueryProtocolError::InvalidKeyValueSection)?;
255 |
256 | // skip 10 meaningless padding bytes
257 | for _ in 0..10 {
258 | res.get_u8();
259 | }
260 |
261 | // players section
262 | let mut players = vec![];
263 | loop {
264 | let username = get_string(&mut res)?;
265 | if username.is_empty() {
266 | break;
267 | }
268 | players.push(username);
269 | }
270 |
271 | Ok(FullStatResponse {
272 | motd,
273 | game_type,
274 | game_id,
275 | version,
276 | plugins,
277 | map,
278 | num_players,
279 | max_players,
280 | host_port,
281 | host_ip,
282 | players,
283 | })
284 | }
285 |
286 | create_timeout!(stat_basic, BasicStatResponse);
287 | create_timeout!(stat_full, FullStatResponse);
288 |
289 | /// Perform a handshake request per
290 | ///
291 | /// # Returns
292 | /// A tuple `(challenge_token, session_id)` to be used in subsequent server interactions
293 | ///
294 | /// # Errors
295 | /// Returns `Err` if there was a network error, or if the returned token was not valid.
296 | async fn handshake(socket: &UdpSocket) -> io::Result<(i32, i32)> {
297 | // generate new token per interaction to avoid reset problems
298 | #[allow(clippy::cast_possible_wrap)] // this is fine, we don't care about the value
299 | let session_id = (random::() & SESSION_ID_MASK) as i32;
300 |
301 | let mut req = BytesMut::with_capacity(7);
302 | req.put_u16(QUERY_MAGIC);
303 | req.put_u8(9); // packet type 9 - handshake
304 | req.put_i32(session_id);
305 | // no payload for handshake requests
306 |
307 | socket.send(&req).await?;
308 |
309 | let mut response = Box::pin(recv_packet(socket)).await?;
310 | validate_packet(&mut response, 9, session_id)?;
311 |
312 | let token_str = get_string(&mut response)?;
313 |
314 | token_str
315 | .parse()
316 | .map(|t| (t, session_id))
317 | .map_err(|_| QueryProtocolError::CannotParseInt.into())
318 | }
319 |
320 | async fn recv_packet(socket: &UdpSocket) -> io::Result {
321 | let mut buf = [0u8; 65536];
322 | socket.recv(&mut buf).await?;
323 |
324 | Ok(Bytes::copy_from_slice(&buf))
325 | }
326 |
327 | fn validate_packet(packet: &mut Bytes, expected_type: u8, expected_session: i32) -> io::Result<()> {
328 | let recv_type = packet.get_u8();
329 | if recv_type != expected_type {
330 | return Err(QueryProtocolError::InvalidPacketType.into());
331 | }
332 |
333 | let recv_session = packet.get_i32();
334 | if recv_session != expected_session {
335 | return Err(QueryProtocolError::SessionIdMismatch.into());
336 | }
337 |
338 | Ok(())
339 | }
340 |
341 | fn get_string(bytes: &mut Bytes) -> io::Result {
342 | let mut buf = vec![];
343 | loop {
344 | let byte = bytes.get_u8();
345 | if byte == 0 {
346 | break;
347 | }
348 | buf.push(byte);
349 | }
350 |
351 | String::from_utf8(buf).map_err(|_| QueryProtocolError::InvalidUtf8.into())
352 | }
353 |
354 | #[cfg(test)]
355 | mod tests {
356 | use tokio::io;
357 |
358 | use super::{stat_basic, stat_full};
359 |
360 | #[tokio::test]
361 | async fn test_stat_basic() -> io::Result<()> {
362 | let response = stat_basic("localhost", 25565).await?;
363 | println!("{response:#?}");
364 |
365 | Ok(())
366 | }
367 |
368 | #[tokio::test]
369 | async fn test_stat_full() -> io::Result<()> {
370 | let response = stat_full("localhost", 25565).await?;
371 | println!("{response:#?}");
372 |
373 | Ok(())
374 | }
375 | }
376 |
--------------------------------------------------------------------------------
/src/rcon.rs:
--------------------------------------------------------------------------------
1 | //! Enables remote command execution for minecraft servers.
2 | //! See the documentation for [`RconClient`] for more information.
3 |
4 | mod client;
5 | mod packet;
6 |
7 | #[allow(clippy::module_name_repetitions)]
8 | pub use client::RconClient;
9 |
10 | const MAX_LEN_CLIENTBOUND: usize = 4096;
11 | const MAX_LEN_SERVERBOUND: usize = 1446;
12 |
--------------------------------------------------------------------------------
/src/rcon/client.rs:
--------------------------------------------------------------------------------
1 | //! Implementation of the [RCON](https://wiki.vg/RCON) protocol.
2 |
3 | use super::{
4 | packet::{RconPacket, RconPacketType},
5 | MAX_LEN_CLIENTBOUND,
6 | };
7 | use crate::errors::{timeout_err, RconProtocolError};
8 | use bytes::{BufMut, BytesMut};
9 | use std::time::Duration;
10 | use tokio::{
11 | io::{self, AsyncReadExt, AsyncWriteExt, Error},
12 | net::TcpStream,
13 | time::timeout,
14 | };
15 |
16 | /// Struct that stores the connection and other state of the RCON protocol with the server.
17 | ///
18 | /// # Examples
19 | ///
20 | /// ```no_run
21 | /// use mc_query::rcon::RconClient;
22 | /// use tokio::io::Result;
23 | ///
24 | /// #[tokio::main]
25 | /// async fn main() -> Result<()> {
26 | /// let mut client = RconClient::new("localhost", 25575).await?;
27 | /// client.authenticate("password").await?;
28 | ///
29 | /// let output = client.run_command("time set day").await?;
30 | /// println!("{output}");
31 | ///
32 | /// Ok(())
33 | /// }
34 | /// ```
35 | #[allow(clippy::module_name_repetitions)]
36 | #[derive(Debug)]
37 | pub struct RconClient {
38 | socket: TcpStream,
39 | timeout: Option,
40 | }
41 |
42 | impl RconClient {
43 | /// Construct an [`RconClient`] that connects to the given host and port.
44 | /// Note: to authenticate use the `authenticate` method, this method does not take a password.
45 | ///
46 | /// Clients constructed this way will wait arbitrarily long (maybe forever!) to recieve
47 | /// a response from the server. To set a timeout, see [`with_timeout`] or [`set_timeout`].
48 | ///
49 | /// # Arguments
50 | /// * `host` - A string slice that holds the hostname of the server to connect to.
51 | /// * `port` - The port to connect to.
52 | ///
53 | /// # Errors
54 | /// Returns `Err` if there was a network error.
55 | pub async fn new(host: &str, port: u16) -> io::Result {
56 | let connection = TcpStream::connect(format!("{host}:{port}")).await?;
57 |
58 | Ok(Self {
59 | socket: connection,
60 | timeout: None,
61 | })
62 | }
63 |
64 | /// Construct an [`RconClient`] that connects to the given host and port, and a connection
65 | /// timeout.
66 | /// Note: to authenticate use the `authenticate` method, this method does not take a password.
67 | ///
68 | /// Note that timeouts are not precise, and may vary on the order of milliseconds, because
69 | /// of the way the async event loop works.
70 | ///
71 | /// # Arguments
72 | /// * `host` - A string slice that holds the hostname of the server to connect to.
73 | /// * `port` - The port to connect to.
74 | /// * `timeout` - A duration to wait for each response to arrive in.
75 | ///
76 | /// # Errors
77 | /// Returns `Err` if there was a network error.
78 | pub async fn with_timeout(host: &str, port: u16, timeout: Duration) -> io::Result {
79 | let mut client = Self::new(host, port).await?;
80 | client.set_timeout(Some(timeout));
81 |
82 | Ok(client)
83 | }
84 |
85 | /// Change the timeout for future requests.
86 | ///
87 | /// # Arguments
88 | /// * `timeout` - an option specifying the duration to wait for a response.
89 | /// if none, the client may wait forever.
90 | pub fn set_timeout(&mut self, timeout: Option) {
91 | self.timeout = timeout;
92 | }
93 |
94 | /// Disconnect from the server and close the RCON connection.
95 | ///
96 | /// # Errors
97 | /// Returns `Err` if there was an issue closing the connection.
98 | pub async fn disconnect(mut self) -> io::Result<()> {
99 | self.socket.shutdown().await
100 | }
101 |
102 | /// Authenticate with the server, with the given password.
103 | ///
104 | /// If authentication fails, this method will return [`RconProtocolError::AuthFailed`].
105 | ///
106 | /// # Arguments
107 | /// * `password` - A string slice that holds the RCON password.
108 | ///
109 | /// # Errors
110 | /// Returns the raw `tokio::io::Error` if there was a network error.
111 | /// Returns an apprpriate [`RconProtocolError`] if the authentication failed for other reasons.
112 | /// Also returns an error if a timeout is set, and the response is not recieved in that timeframe.
113 | pub async fn authenticate(&mut self, password: &str) -> io::Result<()> {
114 | let to = self.timeout;
115 | let fut = self.authenticate_raw(password);
116 |
117 | match to {
118 | None => fut.await,
119 | Some(d) => timeout(d, fut).await.unwrap_or(timeout_err()),
120 | }
121 | }
122 |
123 | /// Run the given command on the server and return the result.
124 | ///
125 | /// # Arguments
126 | /// * `command` - A string slice that holds the command to run. Must be ASCII and under 1446 bytes in length.
127 | ///
128 | /// # Errors
129 | /// Returns an error if there was a network issue or an [`RconProtocolError`] for other failures.
130 | /// Also returns an error if a timeout was set and a response was not recieved in that timeframe.
131 | pub async fn run_command(&mut self, command: &str) -> io::Result {
132 | let to = self.timeout;
133 | let fut = self.run_command_raw(command);
134 |
135 | match to {
136 | None => fut.await,
137 | Some(d) => timeout(d, fut).await.unwrap_or(timeout_err()),
138 | }
139 | }
140 |
141 | async fn authenticate_raw(&mut self, password: &str) -> io::Result<()> {
142 | let packet =
143 | RconPacket::new(1, RconPacketType::Login, password.to_string()).map_err(Error::from)?;
144 |
145 | self.write_packet(packet).await?;
146 |
147 | let packet = self.read_packet().await?;
148 |
149 | if !matches!(packet.packet_type, RconPacketType::RunCommand) {
150 | return Err(RconProtocolError::InvalidPacketType.into());
151 | }
152 |
153 | if packet.request_id == -1 {
154 | return Err(RconProtocolError::AuthFailed.into());
155 | } else if packet.request_id != 1 {
156 | return Err(RconProtocolError::RequestIdMismatch.into());
157 | }
158 |
159 | Ok(())
160 | }
161 |
162 | async fn run_command_raw(&mut self, command: &str) -> io::Result {
163 | let packet = RconPacket::new(1, RconPacketType::RunCommand, command.to_string())
164 | .map_err(Error::from)?;
165 |
166 | self.write_packet(packet).await?;
167 |
168 | let mut full_payload = String::new();
169 |
170 | loop {
171 | let recieved = self.read_packet().await?;
172 |
173 | if recieved.request_id == -1 {
174 | return Err(RconProtocolError::AuthFailed.into());
175 | } else if recieved.request_id != 1 {
176 | return Err(RconProtocolError::RequestIdMismatch.into());
177 | }
178 |
179 | full_payload.push_str(&recieved.payload);
180 |
181 | // wiki says this method of determining if this is the end of the
182 | // response is not 100% reliable, but this is the best solution imo
183 | // if this ends up being a problem, this can be changed later
184 | if recieved.payload.len() < MAX_LEN_CLIENTBOUND {
185 | break;
186 | }
187 | }
188 |
189 | Ok(full_payload)
190 | }
191 |
192 | /// Read a packet from the socket.
193 | async fn read_packet(&mut self) -> io::Result {
194 | let len = self.socket.read_i32_le().await?;
195 |
196 | let mut bytes = BytesMut::new();
197 | bytes.put_i32_le(len);
198 |
199 | for _ in 0..len {
200 | let current = self.socket.read_u8().await?;
201 | bytes.put_u8(current);
202 | }
203 |
204 | RconPacket::try_from(bytes.freeze()).map_err(Error::from)
205 | }
206 |
207 | /// Write a packet to the socket.
208 | ///
209 | /// # Arguments
210 | /// * `packet` - An owned [`RconPacket`] to write to the socket.
211 | async fn write_packet(&mut self, packet: RconPacket) -> io::Result<()> {
212 | let bytes = packet.bytes();
213 |
214 | self.socket.write_all(&bytes).await
215 | }
216 | }
217 |
218 | #[cfg(test)]
219 | mod tests {
220 | use super::RconClient;
221 | use tokio::io;
222 |
223 | #[tokio::test]
224 | async fn test_rcon_command() -> io::Result<()> {
225 | let mut client = RconClient::new("localhost", 25575).await?;
226 | client.authenticate("mc-query-test").await?;
227 | let response = client.run_command("time set day").await?;
228 |
229 | println!("recieved response: {response}");
230 |
231 | Ok(())
232 | }
233 |
234 | #[tokio::test]
235 | async fn test_rcon_unauthenticated() -> io::Result<()> {
236 | let mut client = RconClient::new("localhost", 25575).await?;
237 | let result = client.run_command("time set day").await;
238 |
239 | assert!(result.is_err());
240 |
241 | Ok(())
242 | }
243 |
244 | #[tokio::test]
245 | async fn test_rcon_incorrect_password() -> io::Result<()> {
246 | let mut client = RconClient::new("localhost", 25575).await?;
247 | let result = client.authenticate("incorrect").await;
248 |
249 | assert!(result.is_err());
250 |
251 | Ok(())
252 | }
253 | }
254 |
--------------------------------------------------------------------------------
/src/rcon/packet.rs:
--------------------------------------------------------------------------------
1 | use crate::errors::RconProtocolError;
2 | use bytes::{Buf, BufMut, Bytes, BytesMut};
3 | use std::mem::size_of;
4 |
5 | use super::{MAX_LEN_CLIENTBOUND, MAX_LEN_SERVERBOUND};
6 |
7 | #[derive(Debug)]
8 | pub(super) enum RconPacketType {
9 | Response,
10 | Login,
11 | RunCommand,
12 | }
13 |
14 | impl From for i32 {
15 | fn from(packet_type: RconPacketType) -> Self {
16 | match packet_type {
17 | RconPacketType::Response => 0,
18 | RconPacketType::RunCommand => 2,
19 | RconPacketType::Login => 3,
20 | }
21 | }
22 | }
23 |
24 | impl TryFrom for RconPacketType {
25 | type Error = RconProtocolError;
26 |
27 | fn try_from(value: i32) -> Result {
28 | match value {
29 | 0 => Ok(RconPacketType::Response),
30 | 2 => Ok(RconPacketType::RunCommand),
31 | 3 => Ok(RconPacketType::Login),
32 | _ => Err(RconProtocolError::InvalidPacketType),
33 | }
34 | }
35 | }
36 |
37 | #[derive(Debug)]
38 | pub(super) struct RconPacket {
39 | pub request_id: i32,
40 | pub packet_type: RconPacketType,
41 | pub payload: String,
42 | }
43 |
44 | impl RconPacket {
45 | pub fn new(
46 | request_id: i32,
47 | packet_type: RconPacketType,
48 | payload: String,
49 | ) -> Result {
50 | if !payload.is_ascii() {
51 | return Err(RconProtocolError::NonAsciiPayload);
52 | }
53 |
54 | if payload.len() > Ord::max(MAX_LEN_CLIENTBOUND, MAX_LEN_SERVERBOUND) {
55 | return Err(RconProtocolError::PayloadTooLong);
56 | }
57 |
58 | Ok(Self {
59 | request_id,
60 | packet_type,
61 | payload,
62 | })
63 | }
64 |
65 | pub fn bytes(self) -> Bytes {
66 | Bytes::from(self)
67 | }
68 | }
69 |
70 | impl TryFrom for RconPacket {
71 | type Error = RconProtocolError;
72 |
73 | fn try_from(mut bytes: Bytes) -> Result {
74 | let len = bytes.get_i32_le(); // length of remaining packet (not including this integer)
75 | let request_id = bytes.get_i32_le();
76 | let packet_type = bytes.get_i32_le();
77 |
78 | let mut payload = String::new();
79 | loop {
80 | let current = bytes.get_u8();
81 | if current == 0 {
82 | // null terminated ASCII string, so stop reading here
83 | break;
84 | }
85 |
86 | payload.push(current as char);
87 | }
88 |
89 | // if the payload is already normal ASCII (without 0xa7), no need to
90 | // check each character to be ASCII or 0xa7
91 | if !payload.is_ascii() {
92 | for c in payload.chars() {
93 | // 0xa7 is an acceptable (though non-ASCII) character
94 | if !c.is_ascii() && (c as u8) != 0xa7 {
95 | return Err(RconProtocolError::NonAsciiPayload);
96 | }
97 | }
98 | }
99 |
100 | let pad = bytes.get_u8(); // there must be a remaining 0 byte as padding
101 | if pad != 0 {
102 | return Err(RconProtocolError::InvalidRconResponse);
103 | }
104 |
105 | // validate if the lengths match
106 | if get_remaining_length(&payload) != len {
107 | return Err(RconProtocolError::InvalidRconResponse);
108 | }
109 |
110 | Self::new(request_id, packet_type.try_into()?, payload)
111 | }
112 | }
113 |
114 | impl From for Bytes {
115 | fn from(packet: RconPacket) -> Self {
116 | let len = get_remaining_length(&packet.payload);
117 | let packet_type: i32 = packet.packet_type.into();
118 |
119 | let mut bytes = BytesMut::new();
120 |
121 | bytes.put_i32_le(len);
122 | bytes.put_i32_le(packet.request_id);
123 | bytes.put_i32_le(packet_type);
124 | bytes.put(packet.payload.as_bytes());
125 | bytes.put_u16(0x00_00);
126 |
127 | bytes.freeze()
128 | }
129 | }
130 |
131 | /// Get the *remaining length* of the packet given its payload.
132 | ///
133 | /// Remaining length here refers to the length of the packet in bytes excluding
134 | /// the first four bytes which communicate this value. So it refers to the
135 | /// length of the packet *after* the length field.
136 | ///
137 | /// As the remainder of the packet is composed of two [i32]s (request ID and type),
138 | /// the payload, and **TWO** 0 bytes (because rust strings are not null-terminated),
139 | /// it is the size of two [i32]s + the length of the payload + 2.
140 | fn get_remaining_length(payload: &str) -> i32 {
141 | (payload.len() + size_of::() * 2 + 2) as i32
142 | }
143 |
--------------------------------------------------------------------------------
/src/socket.rs:
--------------------------------------------------------------------------------
1 | use crate::varint::{VarInt, CONTINUE_BIT};
2 | use async_trait::async_trait;
3 | use bytes::{BufMut, BytesMut};
4 | use std::io::{Error, ErrorKind};
5 | use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, Result};
6 |
7 | /// Trait to allow for reading and writing `VarInt`s from the socket.
8 | ///
9 | /// The type is specified [in wiki.vg](https://wiki.vg/Protocol#VarInt_and_VarLong).
10 | #[async_trait]
11 | pub(crate) trait ReadWriteVarInt {
12 | /// Read a [VarInt] from the socket.
13 | /// Returns the parsed value as [i32] in a [Result].
14 | async fn read_varint(&mut self) -> Result;
15 | }
16 |
17 | /// Trait to allow for reading and writing strings from the socket.
18 | ///
19 | /// The format for strings is specified [in this table in wiki.vg](https://wiki.vg/Protocol#Data_types).
20 | /// It is a UTF-8 string prefixed with its size in bytes as a [`VarInt`].
21 | #[async_trait]
22 | pub(crate) trait ReadWriteMinecraftString {
23 | /// Read a [String] from the socket.
24 | /// Returns the parsed value recieved from the socket in a [Result].
25 | async fn read_mc_string(&mut self) -> Result;
26 | }
27 |
28 | #[async_trait]
29 | impl ReadWriteVarInt for T
30 | where
31 | T: AsyncRead + AsyncWrite + Unpin + Send,
32 | {
33 | async fn read_varint(&mut self) -> Result {
34 | let mut bytes = BytesMut::with_capacity(5);
35 |
36 | loop {
37 | let current = self.read_u8().await?;
38 | bytes.put_u8(current);
39 |
40 | if current & CONTINUE_BIT == 0 {
41 | break;
42 | }
43 | }
44 |
45 | VarInt::new(bytes.freeze())
46 | .try_into()
47 | .map_err(|err| Error::new(ErrorKind::InvalidData, err))
48 | }
49 | }
50 |
51 | #[async_trait]
52 | impl ReadWriteMinecraftString for T
53 | where
54 | T: AsyncRead + AsyncWrite + Unpin + Send,
55 | {
56 | async fn read_mc_string(&mut self) -> Result {
57 | let len = self.read_varint().await?;
58 | let mut buffer = vec![0; len as usize];
59 | self.read_exact(&mut buffer).await?;
60 |
61 | String::from_utf8(buffer).map_err(|err| Error::new(ErrorKind::InvalidData, err))
62 | }
63 | }
--------------------------------------------------------------------------------
/src/status.rs:
--------------------------------------------------------------------------------
1 | //! Get the status of a server using the [Server List Ping](https://wiki.vg/Server_List_Ping) protocol.
2 | //! See documentation for [`status`] for more information.
3 |
4 | pub mod data;
5 | mod packet;
6 |
7 | use crate::{
8 | errors::MinecraftProtocolError,
9 | socket::{ReadWriteMinecraftString, ReadWriteVarInt},
10 | varint::VarInt,
11 | };
12 | use tokio::{
13 | io::{self, AsyncWriteExt, Interest},
14 | net::TcpStream,
15 | };
16 |
17 | use self::{
18 | data::StatusResponse,
19 | packet::{Packet, PacketId},
20 | };
21 |
22 | /// Ping the server for information following the [Server List Ping](https://wiki.vg/Server_List_Ping) protocol.
23 | ///
24 | /// # Arguments
25 | /// * `host` - A string slice that holds the hostname of the server to connect to.
26 | /// * `port` - The port to connect to on that server.
27 | ///
28 | /// # Errors
29 | /// Returns `Err` if there was a network issue or the server sent invalid data.
30 | ///
31 | /// # Examples
32 | /// ```
33 | /// use mc_query::status;
34 | /// use tokio::io::Result;
35 | ///
36 | /// #[tokio::main]
37 | /// async fn main() -> Result<()> {
38 | /// let data = status("mc.hypixel.net", 25565).await?;
39 | /// println!("{data:#?}");
40 | ///
41 | /// Ok(())
42 | /// }
43 | /// ```
44 | pub async fn status(host: &str, port: u16) -> io::Result {
45 | let mut socket = TcpStream::connect(format!("{host}:{port}")).await?;
46 |
47 | socket
48 | .ready(Interest::READABLE | Interest::WRITABLE)
49 | .await?;
50 |
51 | // handshake packet
52 | // https://wiki.vg/Server_List_Ping#Handshake
53 | let handshake = Packet::builder(PacketId::Handshake)
54 | .add_varint(&VarInt::from(-1))
55 | .add_string(host)
56 | .add_u16(port)
57 | .add_varint(&VarInt::from(PacketId::Status))
58 | .build();
59 |
60 | socket.write_all(&handshake.bytes()).await?;
61 |
62 | // status request packet
63 | // https://wiki.vg/Server_List_Ping#Status_Request
64 | let status_request = Packet::builder(PacketId::Handshake).build();
65 | socket.write_all(&status_request.bytes()).await?;
66 |
67 | // listen to status response
68 | // https://wiki.vg/Server_List_Ping#Status_Response
69 | let _len = socket.read_varint().await?;
70 | let id = socket.read_varint().await?;
71 |
72 | if id != 0 {
73 | return Err(MinecraftProtocolError::InvalidStatusResponse.into());
74 | }
75 |
76 | let data = socket.read_mc_string().await?;
77 | socket.shutdown().await?;
78 |
79 | serde_json::from_str::(&data)
80 | .map_err(|_| MinecraftProtocolError::InvalidStatusResponse.into())
81 | }
82 |
83 | create_timeout!(status, StatusResponse);
84 |
85 | #[cfg(test)]
86 | mod tests {
87 | use super::status;
88 | use tokio::io::Result;
89 |
90 | #[tokio::test]
91 | async fn test_hypixel_status() -> Result<()> {
92 | let data = status("mc.hypixel.net", 25565).await?;
93 | println!("{data:#?}");
94 |
95 | Ok(())
96 | }
97 |
98 | #[tokio::test]
99 | async fn test_local_status() -> Result<()> {
100 | let data = status("localhost", 25565).await?;
101 | println!("{data:#?}");
102 |
103 | Ok(())
104 | }
105 | }
106 |
--------------------------------------------------------------------------------
/src/status/data.rs:
--------------------------------------------------------------------------------
1 | //! Implementation of the [Server List Ping](https://wiki.vg/Server_List_Ping) protocol
2 |
3 | use serde::{Deserialize, Serialize};
4 |
5 | /// Response from the server with status information.
6 | /// Represents [this JSON object](https://wiki.vg/Server_List_Ping#Status_Response)
7 | /// to be serialized and deserialized.
8 | #[derive(Debug, Serialize, Deserialize)]
9 | pub struct StatusResponse {
10 | /// Information about the game and protocol version.
11 | /// See [Version] for more information.
12 | pub version: Version,
13 |
14 | // Information about players on the server.
15 | /// See [Players] for more information.
16 | pub players: Players,
17 |
18 | /// The "motd" - message shown in the server list by the client.
19 | #[serde(rename = "description")]
20 | pub motd: Option,
21 |
22 | /// URI to the server's favicon.
23 | pub favicon: Option,
24 |
25 | /// Does the server preview chat?
26 | #[serde(rename = "previewsChat")]
27 | pub previews_chat: Option,
28 |
29 | /// Does the server use signed chat messages?
30 | /// Only returned for servers post 1.19.1
31 | #[serde(rename = "enforcesSecureChat")]
32 | pub enforces_secure_chat: Option,
33 | }
34 |
35 | /// Struct that stores information about players on the server.
36 | ///
37 | /// Not intended to be used directly, but only as a part of [`StatusResponse`].
38 | #[derive(Debug, Serialize, Deserialize)]
39 | pub struct Players {
40 | /// The maximum number of players allowed on the server.
41 | pub max: u32,
42 |
43 | /// The number of players currently online.
44 | pub online: u32,
45 |
46 | /// A listing of some online Players.
47 | /// See [Sample] for more information.
48 | pub sample: Option>,
49 | }
50 |
51 | /// A player listed on the server's list ping information.
52 | ///
53 | /// Not intended to be used directly, but only as a part of [`StatusResponse`].
54 | #[derive(Debug, Serialize, Deserialize)]
55 | pub struct Sample {
56 | /// The player's username.
57 | pub name: String,
58 |
59 | /// The player's UUID.
60 | pub id: String,
61 | }
62 |
63 | /// Struct that stores version information about the server.
64 | ///
65 | /// Not intended to be used directly, but only as a part of [`StatusResponse`].
66 | #[derive(Debug, Serialize, Deserialize)]
67 | pub struct Version {
68 | /// The game version (e.g: 1.19.1)
69 | pub name: String,
70 | /// The version of the [Protocol](https://wiki.vg/Protocol) being used.
71 | ///
72 | /// See [the wiki.vg page](https://wiki.vg/Protocol_version_numbers) for a
73 | /// reference on what versions these correspond to.
74 | pub protocol: i64,
75 | }
76 |
77 | /// Represents a chat object (the MOTD is sent as a chat object).
78 | #[derive(Debug, Serialize, Deserialize)]
79 | #[serde(untagged)]
80 | pub enum ChatObject {
81 | /// An individual chat object
82 | Object(ChatComponentObject),
83 |
84 | /// Vector of multiple chat objects
85 | Array(Vec),
86 |
87 | /// Unknown data - raw JSON
88 | JsonPrimitive(serde_json::Value),
89 | }
90 |
91 | /// A piece of a `ChatObject`
92 | #[derive(Debug, Serialize, Deserialize)]
93 | pub struct ChatComponentObject {
94 | /// Text of the chat message
95 | pub text: Option,
96 |
97 | /// Translation key if the message needs to pull from the language file.
98 | /// See [wiki.vg](https://wiki.vg/Chat#Translation_component)
99 | pub translate: Option,
100 |
101 | /// Displays the keybind for the specified key, or the string itself if unknown.
102 | pub keybind: Option,
103 |
104 | /// Should the text be rendered **bold**?
105 | pub bold: Option,
106 |
107 | /// Should the text be rendered *italic*?
108 | pub italic: Option,
109 |
110 | /// Should the text be rendered __underlined__?
111 | pub underlined: Option,
112 |
113 | /// Should the text be rendered as ~~strikethrough~~
114 | pub strikethrough: Option,
115 |
116 | /// Should the text be rendered as obfuscated?
117 | /// Switching randomly between characters of the same width
118 | pub obfuscated: Option,
119 |
120 | /// The font to use to render, comes in three options:
121 | /// * `minecraft:uniform` - Unicode font
122 | /// * `minecraft:alt` - enchanting table font
123 | /// * `minecraft:default` - font based on resource pack (1.16+)
124 | ///
125 | /// Any other value can be ignored
126 | pub font: Option,
127 |
128 | /// The color to display the chat item in.
129 | /// Can be a [chat color](https://wiki.vg/Chat#Colors),
130 | /// [format code](https://wiki.vg/Chat#Styles),
131 | /// or any valid web color
132 | pub color: Option,
133 |
134 | /// Text to insert into the chat box when shift-clicking this component
135 | pub insertion: Option,
136 |
137 | /// Defines an event that occurs when this chat item is clicked
138 | #[serde(rename = "clickEvent")]
139 | pub click_event: Option,
140 |
141 | /// Defines an event that occurs when this chat item is hovered on
142 | #[serde(rename = "hoverEvent")]
143 | pub hover_event: Option,
144 |
145 | /// Sibling components to this chat item.
146 | /// If present, will not be empty
147 | pub extra: Option>,
148 | }
149 |
150 | /// `ClickEvent` data for a chat component
151 | #[derive(Debug, Serialize, Deserialize)]
152 | pub struct ChatClickEvent {
153 | // These are not renamed on purpose. (server returns them in snake_case)
154 | /// Opens the URL in the user's default browser. Protocol must be `http` or `https`
155 | pub open_url: Option,
156 |
157 | /// Runs the command.
158 | /// Simply causes the user to say the string in chat -
159 | /// so only has command effect if it starts with /
160 | ///
161 | /// Irrelevant for motd purposes.
162 | pub run_command: Option,
163 |
164 | /// Replaces the content of the user's chat box with the given text.
165 | ///
166 | /// Irrelevant for motd purposes.
167 | pub suggest_command: Option,
168 |
169 | /// Copies the given text into the client's clipboard.
170 | pub copy_to_clipboard: Option,
171 | }
172 |
173 | /// `HoverEvent` data for a chat component
174 | #[derive(Debug, Serialize, Deserialize)]
175 | pub struct ChatHoverEvent {
176 | // These are not renamed on purpose. (server returns them in snake_case)
177 | /// Text to show when the item is hovered over
178 | pub show_text: Option>,
179 |
180 | /// Same as `show_text`, but for servers < 1.16
181 | pub value: Option>,
182 |
183 | /// Displays the item of the given NBT
184 | pub show_item: Option,
185 |
186 | /// Displays information about the entity with the given NBT
187 | pub show_entity: Option,
188 | }
189 |
--------------------------------------------------------------------------------
/src/status/packet.rs:
--------------------------------------------------------------------------------
1 | use crate::{errors::MinecraftProtocolError, varint::VarInt};
2 | use bytes::{BufMut, Bytes, BytesMut};
3 |
4 | #[derive(Debug)]
5 | pub(super) enum PacketId {
6 | Handshake = 0,
7 | Status = 1,
8 | }
9 |
10 | impl From for u8 {
11 | fn from(id: PacketId) -> Self {
12 | match id {
13 | PacketId::Handshake => 0,
14 | PacketId::Status => 1,
15 | }
16 | }
17 | }
18 |
19 | impl TryFrom for PacketId {
20 | type Error = MinecraftProtocolError;
21 |
22 | fn try_from(value: u8) -> Result {
23 | match value {
24 | 0 => Ok(Self::Handshake),
25 | 1 => Ok(Self::Status),
26 | _ => Err(MinecraftProtocolError::InvalidState),
27 | }
28 | }
29 | }
30 |
31 | impl From for VarInt {
32 | fn from(id: PacketId) -> Self {
33 | let number: u8 = id.into();
34 | VarInt::from(number as i32)
35 | }
36 | }
37 |
38 | #[derive(Debug)]
39 | pub(super) struct Packet {
40 | id: u8,
41 | payload: Bytes,
42 | }
43 |
44 | impl Packet {
45 | pub fn builder(id: PacketId) -> PacketBuilder {
46 | PacketBuilder::new(id)
47 | }
48 |
49 | pub fn bytes(self) -> Bytes {
50 | self.into()
51 | }
52 | }
53 |
54 | impl From for Bytes {
55 | fn from(packet: Packet) -> Self {
56 | let len: i32 = (VarInt::from(packet.id as i32).bytes().len() + packet.payload.len()) as i32;
57 | let mut bytes = BytesMut::new();
58 |
59 | bytes.extend_from_slice(&VarInt::from(len).bytes());
60 | bytes.put_u8(packet.id);
61 | bytes.extend_from_slice(&packet.payload);
62 |
63 | bytes.freeze()
64 | }
65 | }
66 |
67 | #[derive(Debug)]
68 | pub(super) struct PacketBuilder {
69 | id: PacketId,
70 | bytes: BytesMut,
71 | }
72 |
73 | impl PacketBuilder {
74 | pub fn new(id: PacketId) -> Self {
75 | Self {
76 | id,
77 | bytes: BytesMut::new(),
78 | }
79 | }
80 |
81 | pub fn add_varint(mut self, varint: &VarInt) -> Self {
82 | self.bytes.extend_from_slice(varint);
83 | self
84 | }
85 |
86 | pub fn add_string(self, string: &str) -> Self {
87 | let mut inst = self.add_varint(&VarInt::from(string.len() as i32));
88 | inst.bytes.put(string.as_bytes());
89 | inst
90 | }
91 |
92 | pub fn add_u16(mut self, short: u16) -> Self {
93 | self.bytes.put_u16(short);
94 | self
95 | }
96 |
97 | pub fn build(self) -> Packet {
98 | Packet {
99 | id: self.id.into(),
100 | payload: self.bytes.freeze(),
101 | }
102 | }
103 | }
104 |
--------------------------------------------------------------------------------
/src/varint.rs:
--------------------------------------------------------------------------------
1 | use bytes::Bytes;
2 | use std::ops::Deref;
3 |
4 | use crate::errors::MinecraftProtocolError;
5 |
6 | pub(crate) const SEGMENT_BITS: u8 = 0x7f; // 0111 1111
7 | pub(crate) const CONTINUE_BIT: u8 = 0x80; // 1000 0000
8 |
9 | pub(crate) struct VarInt {
10 | bytes: Bytes,
11 | }
12 |
13 | impl VarInt {
14 | pub(crate) fn new(bytes: Bytes) -> Self {
15 | Self { bytes }
16 | }
17 |
18 | pub(crate) fn bytes(&self) -> Bytes {
19 | self.bytes.clone()
20 | }
21 | }
22 |
23 | impl From for VarInt {
24 | fn from(value: i32) -> Self {
25 | let mut value = (value as u64) & 0xffff_ffff;
26 | let mut buffer = vec![];
27 |
28 | loop {
29 | let temp = (value & SEGMENT_BITS as u64) as u8;
30 | value >>= 7;
31 |
32 | if value != 0 {
33 | buffer.push(temp | CONTINUE_BIT);
34 | } else {
35 | buffer.push(temp);
36 | }
37 |
38 | if value == 0 {
39 | break;
40 | }
41 | }
42 |
43 | Self {
44 | bytes: Bytes::from(Box::from(buffer)),
45 | }
46 | }
47 | }
48 |
49 | impl TryInto for VarInt {
50 | type Error = MinecraftProtocolError;
51 |
52 | fn try_into(self) -> Result {
53 | let mut value: i32 = 0;
54 | let mut position = 0;
55 |
56 | for current_byte in self.bytes {
57 | value |= ((current_byte & SEGMENT_BITS) as i32) << position;
58 |
59 | if current_byte & CONTINUE_BIT == 0 {
60 | return Ok(value);
61 | }
62 |
63 | position += 7;
64 | if position >= 32 {
65 | return Err(MinecraftProtocolError::InvalidVarInt);
66 | }
67 | }
68 |
69 | unreachable!();
70 | }
71 | }
72 |
73 | impl From for Bytes {
74 | fn from(varint: VarInt) -> Self {
75 | varint.bytes
76 | }
77 | }
78 |
79 | impl Deref for VarInt {
80 | type Target = [u8];
81 |
82 | fn deref(&self) -> &Self::Target {
83 | &self.bytes
84 | }
85 | }
86 |
87 | #[cfg(test)]
88 | mod tests {
89 | use super::VarInt;
90 | use crate::errors::MinecraftProtocolError;
91 | use bytes::Bytes;
92 | use std::collections::HashMap;
93 |
94 | #[test]
95 | fn test_into_varint() {
96 | let cases = HashMap::from([
97 | (0, b"\x00".as_slice()),
98 | (1, b"\x01"),
99 | (2, b"\x02"),
100 | (127, b"\x7f"),
101 | (128, b"\x80\x01"),
102 | (255, b"\xff\x01"),
103 | (25565, b"\xdd\xc7\x01"),
104 | (2097151, b"\xff\xff\x7f"),
105 | (i32::MAX, b"\xff\xff\xff\xff\x07"),
106 | (-1, b"\xff\xff\xff\xff\x0f"),
107 | (i32::MIN, b"\x80\x80\x80\x80\x08"),
108 | ]);
109 |
110 | for (k, v) in cases {
111 | let varint: VarInt = k.into();
112 | assert_eq!(varint.bytes.len(), v.len());
113 | assert_eq!(varint.bytes, v);
114 | }
115 | }
116 |
117 | #[test]
118 | fn test_from_varint() {
119 | let cases = HashMap::from([
120 | (0, b"\x00".as_slice()),
121 | (1, b"\x01"),
122 | (2, b"\x02"),
123 | (127, b"\x7f"),
124 | (128, b"\x80\x01"),
125 | (255, b"\xff\x01"),
126 | (25565, b"\xdd\xc7\x01"),
127 | (2097151, b"\xff\xff\x7f"),
128 | (i32::MAX, b"\xff\xff\xff\xff\x07"),
129 | (-1, b"\xff\xff\xff\xff\x0f"),
130 | (i32::MIN, b"\x80\x80\x80\x80\x08"),
131 | ])
132 | .into_iter()
133 | .map(|(k, v)| {
134 | (
135 | k,
136 | VarInt {
137 | bytes: Bytes::from(v),
138 | },
139 | )
140 | })
141 | .collect::>();
142 |
143 | for (k, v) in cases {
144 | let x: Result = v.try_into();
145 |
146 | if let Err(MinecraftProtocolError::InvalidVarInt) = x {
147 | panic!("{k} as VarInt returned Err during conversion");
148 | }
149 | let x = x.unwrap();
150 |
151 | assert_eq!(x, k);
152 | }
153 | }
154 | }
155 |
--------------------------------------------------------------------------------
/test:
--------------------------------------------------------------------------------
1 | #! /usr/bin/env python3
2 |
3 | from argparse import ArgumentParser
4 | from hashlib import sha1
5 | from os import mkdir
6 | from pathlib import Path
7 | from signal import SIGINT, signal
8 | from shutil import which
9 | from subprocess import PIPE, run, Popen
10 | from sys import argv, stderr, exit
11 |
12 | JAR_URL = "https://piston-data.mojang.com/v1/objects/450698d1863ab5180c25d7c804ef0fe6369dd1ba/server.jar"
13 | JAR_SHA1 = JAR_URL.split("/")[-2]
14 |
15 | parser = ArgumentParser(description="mc-query test utility", prog=argv[0])
16 | parser.add_argument(
17 | "--server-only", help="runs server only, without running cargo tests"
18 | )
19 | args = parser.parse_args()
20 |
21 | if args.server_only:
22 | print("running server only, not cargo tests.")
23 |
24 | print("ensuring server directory exists...")
25 | if not Path("server").is_dir():
26 | mkdir("server")
27 |
28 | print("checking for server.jar...")
29 | if not Path("server/server.jar").is_file():
30 | print("server/server.jar not found... fetching server")
31 | run([which("curl"), JAR_URL, "-o", "server/server.jar"])
32 | else:
33 | print("server.jar found. validating...")
34 |
35 | sha1sum = sha1()
36 | with open("server/server.jar", "rb") as f:
37 | block = f.read(2**16)
38 | while len(block) != 0:
39 | sha1sum.update(block)
40 | block = f.read(2**16)
41 |
42 | if sha1sum.hexdigest() != JAR_SHA1:
43 | print("could not verify integrity of server.jar... exiting...", file=stderr)
44 | exit(1)
45 |
46 | print("ensuring configuration files...")
47 | with open("server/eula.txt", "w") as f:
48 | f.writelines(["eula=true"])
49 |
50 | with open("server/server.properties", "w") as f, open(
51 | "resources/server.properties", "r"
52 | ) as source:
53 | f.write(source.read())
54 |
55 | print("starting server...")
56 | process = Popen(
57 | [which("java"), "-Xmx1G", "-jar", "server.jar", "nogui"],
58 | cwd="./server",
59 | stdin=PIPE,
60 | stdout=PIPE,
61 | stderr=PIPE,
62 | text=True,
63 | )
64 |
65 |
66 | def signal_handler(_s, _f):
67 | global process
68 | process.kill()
69 |
70 |
71 | signal(SIGINT, signal_handler)
72 |
73 | for line in process.stdout:
74 | print(f"server log: {line}", end="")
75 | if (not args.server_only) and "RCON running on 0.0.0.0:25575" in line:
76 | print("server RCON is ready, starting tests")
77 | break
78 |
79 | if not args.server_only:
80 | run([which("cargo"), "test", "--", "--show-output"])
81 |
82 | process.kill()
83 |
--------------------------------------------------------------------------------