Skip to content

Commit 15d4aed

Browse files
Support pre-1.7 Java servers (#1059)
Co-authored-by: PerchunPak <git@perchun.it>
1 parent 932766a commit 15d4aed

File tree

12 files changed

+469
-36
lines changed

12 files changed

+469
-36
lines changed

README.md

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ python3 -m pip install mcstatus
2121

2222
### Python API
2323

24-
#### Java Edition
24+
#### Java Edition (1.7+)
2525

2626
```python
2727
from mcstatus import JavaServer
@@ -48,6 +48,20 @@ query = server.query()
4848
print(f"The server has the following players online: {', '.join(query.players.names)}")
4949
```
5050

51+
#### Java Edition (1.4-1.6)
52+
53+
```python
54+
from mcstatus import LegacyServer
55+
56+
# You can pass the same address you'd enter into the address field in minecraft into the 'lookup' function
57+
# If you know the host and port, you may skip this and use LegacyServer("example.org", 1234)
58+
server = LegacyServer.lookup("example.org:1234")
59+
60+
# 'status' is supported by all Minecraft servers.
61+
status = server.status()
62+
print(f"The server has {status.players.online} player(s) online and replied in {status.latency} ms")
63+
```
64+
5165
#### Bedrock Edition
5266

5367
```python

docs/api/basic.rst

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,21 @@ These are classes, that you use to send a request to server.
1515
:undoc-members:
1616
:show-inheritance:
1717

18+
.. autoclass:: mcstatus.server.BaseJavaServer
19+
:members:
20+
:undoc-members:
21+
:show-inheritance:
22+
1823
.. autoclass:: mcstatus.server.JavaServer
1924
:members:
2025
:undoc-members:
2126
:show-inheritance:
2227

28+
.. autoclass:: mcstatus.server.LegacyServer
29+
:members:
30+
:undoc-members:
31+
:show-inheritance:
32+
2333
.. autoclass:: mcstatus.server.BedrockServer
2434
:members:
2535
:undoc-members:
@@ -31,8 +41,8 @@ Response Objects
3141

3242
These are the classes that you get back after making a request.
3343

34-
For Java Server
35-
***************
44+
For Java Server (1.7+)
45+
**********************
3646

3747
.. module:: mcstatus.responses
3848

@@ -79,6 +89,33 @@ For Java Server
7989
:exclude-members: build
8090

8191

92+
For Java Server (1.4-1.6)
93+
*************************
94+
95+
.. versionadded:: 12.1.0
96+
97+
.. module:: mcstatus.responses
98+
:no-index:
99+
100+
.. autoclass:: mcstatus.responses.LegacyStatusResponse()
101+
:members:
102+
:undoc-members:
103+
:inherited-members:
104+
:exclude-members: build
105+
106+
.. autoclass:: mcstatus.responses.LegacyStatusPlayers()
107+
:members:
108+
:undoc-members:
109+
:inherited-members:
110+
:exclude-members: build
111+
112+
.. autoclass:: mcstatus.responses.LegacyStatusVersion()
113+
:members:
114+
:undoc-members:
115+
:inherited-members:
116+
:exclude-members: build
117+
118+
82119
For Bedrock Servers
83120
*******************
84121

docs/api/internal.rst

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,16 @@ versions. They are only documented here for linkable reference to them.
1616
:undoc-members:
1717
:show-inheritance:
1818

19+
.. autoclass:: mcstatus.legacy_status.LegacyServerStatus
20+
:members:
21+
:undoc-members:
22+
:show-inheritance:
23+
24+
.. autoclass:: mcstatus.legacy_status.AsyncLegacyServerStatus
25+
:members:
26+
:undoc-members:
27+
:show-inheritance:
28+
1929
.. autoclass:: mcstatus.bedrock_status.BedrockServerStatus
2030
:members:
2131
:undoc-members:

mcstatus/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
from mcstatus.server import BedrockServer, JavaServer, MCServer
1+
from mcstatus.server import BedrockServer, JavaServer, LegacyServer, MCServer
22

33
__all__ = [
44
"BedrockServer",
55
"JavaServer",
6+
"LegacyServer",
67
"MCServer",
78
]

mcstatus/__main__.py

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,11 @@
88
import dataclasses
99
from typing import TypeAlias
1010

11-
from mcstatus import JavaServer, BedrockServer
11+
from mcstatus import JavaServer, LegacyServer, BedrockServer
1212
from mcstatus.responses import JavaStatusResponse
1313
from mcstatus.motd import Motd
1414

15-
SupportedServers: TypeAlias = "JavaServer | BedrockServer"
15+
SupportedServers: TypeAlias = "JavaServer | LegacyServer | BedrockServer"
1616

1717
PING_PACKET_FAIL_WARNING = (
1818
"warning: contacting {address} failed with a 'ping' packet but succeeded with a 'status' packet,\n"
@@ -39,15 +39,17 @@ def _motd(motd: Motd) -> str:
3939
def _kind(serv: SupportedServers) -> str:
4040
if isinstance(serv, JavaServer):
4141
return "Java"
42+
elif isinstance(serv, LegacyServer):
43+
return "Java (pre-1.7)"
4244
elif isinstance(serv, BedrockServer):
4345
return "Bedrock"
4446
else:
4547
raise ValueError(f"unsupported server for kind: {serv}")
4648

4749

4850
def _ping_with_fallback(server: SupportedServers) -> float:
49-
# bedrock doesn't have ping method
50-
if isinstance(server, BedrockServer):
51+
# only Java has ping method
52+
if not isinstance(server, JavaServer):
5153
return server.status().latency
5254

5355
# try faster ping packet first, falling back to status with a warning.
@@ -161,14 +163,17 @@ def main(argv: list[str] = sys.argv[1:]) -> int:
161163
parser = argparse.ArgumentParser(
162164
"mcstatus",
163165
description="""
164-
mcstatus provides an easy way to query 1.7 or newer Minecraft servers for any
165-
information they can expose. It provides three modes of access: query, status,
166-
ping and json.
166+
mcstatus provides an easy way to query Minecraft servers for any information
167+
they can expose. It provides three modes of access: query, status, ping and json.
167168
""",
168169
)
169170

170171
parser.add_argument("address", help="The address of the server.")
171-
parser.add_argument("--bedrock", help="Specifies that 'address' is a Bedrock server (default: Java).", action="store_true")
172+
group = parser.add_mutually_exclusive_group()
173+
group.add_argument("--bedrock", help="Specifies that 'address' is a Bedrock server (default: Java).", action="store_true")
174+
group.add_argument(
175+
"--legacy", help="Specifies that 'address' is a pre-1.7 Java server (default: 1.7+).", action="store_true"
176+
)
172177

173178
subparsers = parser.add_subparsers(title="commands", description="Command to run, defaults to 'status'.")
174179
parser.set_defaults(func=status_cmd)
@@ -184,7 +189,12 @@ def main(argv: list[str] = sys.argv[1:]) -> int:
184189
).set_defaults(func=json_cmd)
185190

186191
args = parser.parse_args(argv)
187-
lookup = JavaServer.lookup if not args.bedrock else BedrockServer.lookup
192+
if args.bedrock:
193+
lookup = BedrockServer.lookup
194+
elif args.legacy:
195+
lookup = LegacyServer.lookup
196+
else:
197+
lookup = JavaServer.lookup
188198

189199
try:
190200
server = lookup(args.address)

mcstatus/legacy_status.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
from time import perf_counter
2+
3+
from mcstatus.protocol.connection import BaseAsyncReadSyncWriteConnection, BaseSyncConnection
4+
from mcstatus.responses import LegacyStatusResponse
5+
6+
7+
class _BaseLegacyServerStatus:
8+
request_status_data = bytes.fromhex(
9+
# see https://minecraft.wiki/w/Java_Edition_protocol/Server_List_Ping#Client_to_server
10+
"fe01fa"
11+
)
12+
13+
@staticmethod
14+
def parse_response(data: bytes, latency: float) -> LegacyStatusResponse:
15+
decoded_data = data.decode("UTF-16BE").split("\0")
16+
if decoded_data[0] != "§1":
17+
raise IOError("Recieved invalid kick packet reason")
18+
19+
return LegacyStatusResponse.build(decoded_data[1:], latency)
20+
21+
22+
class LegacyServerStatus(_BaseLegacyServerStatus):
23+
def __init__(self, connection: BaseSyncConnection):
24+
self.connection = connection
25+
26+
def read_status(self) -> LegacyStatusResponse:
27+
"""Send the status request and read the response."""
28+
start = perf_counter()
29+
self.connection.write(self.request_status_data)
30+
id = self.connection.read(1)
31+
if id != b"\xff":
32+
raise IOError("Received invalid packet ID")
33+
length = self.connection.read_ushort()
34+
data = self.connection.read(length * 2)
35+
end = perf_counter()
36+
return self.parse_response(data, (end - start) * 1000)
37+
38+
39+
class AsyncLegacyServerStatus(_BaseLegacyServerStatus):
40+
def __init__(self, connection: BaseAsyncReadSyncWriteConnection):
41+
self.connection = connection
42+
43+
async def read_status(self) -> LegacyStatusResponse:
44+
"""Send the status request and read the response."""
45+
start = perf_counter()
46+
self.connection.write(self.request_status_data)
47+
id = await self.connection.read(1)
48+
if id != b"\xff":
49+
raise IOError("Received invalid packet ID")
50+
length = await self.connection.read_ushort()
51+
data = await self.connection.read(length * 2)
52+
end = perf_counter()
53+
return self.parse_response(data, (end - start) * 1000)

mcstatus/responses.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,9 @@ class RawQueryResponse(TypedDict):
7979
"JavaStatusPlayers",
8080
"JavaStatusResponse",
8181
"JavaStatusVersion",
82+
"LegacyStatusPlayers",
83+
"LegacyStatusResponse",
84+
"LegacyStatusVersion",
8285
"QueryResponse",
8386
]
8487

@@ -191,6 +194,36 @@ def build(cls, raw: RawJavaResponse, latency: float = 0) -> Self:
191194
)
192195

193196

197+
@dataclass(frozen=True)
198+
class LegacyStatusResponse(BaseStatusResponse):
199+
"""The response object for :meth:`LegacyServerStatus.status() <mcstatus.server.LegacyServer.status>`."""
200+
201+
players: LegacyStatusPlayers
202+
version: LegacyStatusVersion
203+
204+
@classmethod
205+
def build(cls, decoded_data: list[str], latency: float) -> Self:
206+
"""Build BaseStatusResponse and check is it valid.
207+
208+
:param decoded_data: Raw decoded response object.
209+
:param latency: Latency of the request.
210+
:return: :class:`LegacyStatusResponse` object.
211+
"""
212+
213+
return cls(
214+
players=LegacyStatusPlayers(
215+
online=int(decoded_data[3]),
216+
max=int(decoded_data[4]),
217+
),
218+
version=LegacyStatusVersion(
219+
name=decoded_data[1],
220+
protocol=int(decoded_data[0]),
221+
),
222+
motd=Motd.parse(decoded_data[2]),
223+
latency=latency,
224+
)
225+
226+
194227
@dataclass(frozen=True)
195228
class BedrockStatusResponse(BaseStatusResponse):
196229
"""The response object for :meth:`BedrockServer.status() <mcstatus.server.BedrockServer.status>`."""
@@ -284,6 +317,11 @@ def build(cls, raw: RawJavaResponsePlayers) -> Self:
284317
)
285318

286319

320+
@dataclass(frozen=True)
321+
class LegacyStatusPlayers(BaseStatusPlayers):
322+
"""Class for storing information about players on the server."""
323+
324+
287325
@dataclass(frozen=True)
288326
class BedrockStatusPlayers(BaseStatusPlayers):
289327
"""Class for storing information about players on the server."""
@@ -350,6 +388,11 @@ def build(cls, raw: RawJavaResponseVersion) -> Self:
350388
return cls(name=raw["name"], protocol=raw["protocol"])
351389

352390

391+
@dataclass(frozen=True)
392+
class LegacyStatusVersion(BaseStatusVersion):
393+
"""A class for storing version information."""
394+
395+
353396
@dataclass(frozen=True)
354397
class BedrockStatusVersion(BaseStatusVersion):
355398
"""A class for storing version information."""

0 commit comments

Comments
 (0)