Files

131 lines
4.2 KiB
Python

#!/usr/bin/env python3
"""Prueft, ob Java- und Bedrock-Endpunkt eines Crossplay-Servers antworten.
python scripts/check-crossplay.py # localhost
python scripts/check-crossplay.py minecraft.djnizz.at # Live-Server
Der Java-Test spricht das Server-List-Ping-Protokoll, der Bedrock-Test einen
RakNet Unconnected Ping - also genau das, was die echten Clients tun.
"""
import json
import os
import socket
import struct
import sys
import time
HOST = sys.argv[1] if len(sys.argv) > 1 else "127.0.0.1"
JAVA_PORT = int(sys.argv[2]) if len(sys.argv) > 2 else 25565
BEDROCK_PORT = int(sys.argv[3]) if len(sys.argv) > 3 else 19132
TIMEOUT = 10
def _varint(n):
out = b""
while True:
b = n & 0x7F
n >>= 7
out += bytes([b | (0x80 if n else 0)])
if not n:
return out
def _read_varint(sock):
n = 0
for i in range(5):
b = sock.recv(1)
if not b:
raise EOFError("Verbindung vorzeitig geschlossen")
n |= (b[0] & 0x7F) << (7 * i)
if not b[0] & 0x80:
return n
raise ValueError("VarInt zu lang")
def java_ping(host, port):
s = socket.create_connection((host, port), timeout=TIMEOUT)
try:
host_b = host.encode()
# Handshake: Protokollversion ist beim Status-Ping beliebig
payload = (b"\x00" + _varint(772) + _varint(len(host_b)) + host_b
+ struct.pack(">H", port) + _varint(1))
s.sendall(_varint(len(payload)) + payload)
s.sendall(_varint(1) + b"\x00") # Status Request
_read_varint(s) # Paketlaenge
_read_varint(s) # Paket-ID
n = _read_varint(s) # JSON-Laenge
data = b""
while len(data) < n:
chunk = s.recv(n - len(data))
if not chunk:
break
data += chunk
return json.loads(data.decode("utf-8"))
finally:
s.close()
RAKNET_MAGIC = bytes([0x00, 0xFF, 0xFF, 0x00, 0xFE, 0xFE, 0xFE, 0xFE,
0xFD, 0xFD, 0xFD, 0xFD, 0x12, 0x34, 0x56, 0x78])
def bedrock_ping(host, port):
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.settimeout(TIMEOUT)
try:
pkt = (b"\x01" + struct.pack(">Q", int(time.time() * 1000))
+ RAKNET_MAGIC + os.urandom(8))
s.sendto(pkt, (host, port))
data, _ = s.recvfrom(4096)
if data[0] != 0x1C:
raise ValueError(f"unerwartete Antwort-ID 0x{data[0]:02x}")
strlen = struct.unpack(">H", data[33:35])[0]
return data[35:35 + strlen].decode("utf-8", "replace")
finally:
s.close()
def main():
print(f"Ziel: {HOST} (Java {JAVA_PORT}/TCP, Bedrock {BEDROCK_PORT}/UDP)\n")
failed = False
print("=== Java Edition (TCP) ===")
try:
j = java_ping(HOST, JAVA_PORT)
version = j.get("version", {})
desc = j.get("description")
if isinstance(desc, dict):
desc = desc.get("text", "") or "".join(
e.get("text", "") for e in desc.get("extra", []))
players = j.get("players", {})
print(f" OK {version.get('name')} (Protokoll {version.get('protocol')})")
print(f" MOTD {desc!r}")
print(f" Spieler {players.get('online')}/{players.get('max')}")
except Exception as e:
failed = True
print(f" FEHLER {type(e).__name__}: {e}")
print(" -> Security List (TCP 25565), Instanz-Firewall, Container laeuft?")
print("\n=== Bedrock Edition (UDP, RakNet) ===")
try:
fields = bedrock_ping(HOST, BEDROCK_PORT).split(";")
labels = ["Edition", "MOTD Zeile 1", "Protokoll", "Version", "Spieler",
"Max. Spieler", "Server-GUID", "MOTD Zeile 2", "Gamemode"]
print(" OK")
for label, value in zip(labels, fields):
print(f" {label:<13} {value}")
except Exception as e:
failed = True
print(f" FEHLER {type(e).__name__}: {e}")
print(" -> Security List (UDP 19132!), Geyser gestartet?")
print()
if failed:
print("Mindestens ein Endpunkt antwortet nicht.")
sys.exit(1)
print("Beide Editionen erreichbar.")
if __name__ == "__main__":
main()