mirror of
https://github.com/Dinnerbone/mcstatus.git
synced 2026-04-06 03:51:23 +08:00
Added initial work for server pinging. Normal pings work, Query NYI.
This commit is contained in:
54
mcstatus/pinger.py
Normal file
54
mcstatus/pinger.py
Normal file
@@ -0,0 +1,54 @@
|
||||
import datetime
|
||||
import random
|
||||
|
||||
from mcstatus.protocol.connection import Connection
|
||||
|
||||
|
||||
class ServerPinger:
|
||||
def __init__(self, connection, host="", port=0, version=47, ping_token=None):
|
||||
if ping_token is None:
|
||||
ping_token = random.randint(0, 1 << 63 - 1)
|
||||
self.version = version
|
||||
self.connection = connection
|
||||
self.host = host
|
||||
self.port = port
|
||||
self.ping_token = ping_token
|
||||
|
||||
def handshake(self):
|
||||
packet = Connection()
|
||||
packet.write_varint(0)
|
||||
packet.write_varint(self.version)
|
||||
packet.write_utf(self.host)
|
||||
packet.write_short(self.port)
|
||||
packet.write_varint(1) # Intention to query status
|
||||
|
||||
self.connection.write_buffer(packet)
|
||||
|
||||
def read_status(self):
|
||||
request = Connection()
|
||||
request.write_varint(0) # Request status
|
||||
self.connection.write_buffer(request)
|
||||
|
||||
response = self.connection.read_buffer()
|
||||
if response.read_varint() != 0:
|
||||
raise IOError("Received invalid query response packet.")
|
||||
return response.read_utf()
|
||||
|
||||
def test_ping(self):
|
||||
request = Connection()
|
||||
request.write_varint(1) # Test ping
|
||||
request.write_long(self.ping_token)
|
||||
sent = datetime.datetime.now()
|
||||
self.connection.write_buffer(request)
|
||||
|
||||
response = self.connection.read_buffer()
|
||||
received = datetime.datetime.now()
|
||||
if response.read_varint() != 1:
|
||||
raise IOError("Received invalid ping response packet.")
|
||||
received_token = response.read_long()
|
||||
if received_token != self.ping_token:
|
||||
raise IOError("Received mangled ping response packet (expected token %d, received %d)" % (self.ping_token, received_token))
|
||||
|
||||
delta = (received - sent)
|
||||
# We have no trivial way of getting a time delta :(
|
||||
return (delta.days * 24 * 60 * 60 + delta.seconds) * 1000 + delta.microseconds / 1000.0
|
||||
0
mcstatus/protocol/__init__.py
Normal file
0
mcstatus/protocol/__init__.py
Normal file
115
mcstatus/protocol/connection.py
Normal file
115
mcstatus/protocol/connection.py
Normal file
@@ -0,0 +1,115 @@
|
||||
import socket
|
||||
import struct
|
||||
|
||||
|
||||
class Connection:
|
||||
def __init__(self):
|
||||
self.sent = bytearray()
|
||||
self.received = bytearray()
|
||||
|
||||
def read(self, length):
|
||||
result = ""
|
||||
|
||||
result = self.received[:length]
|
||||
self.received = self.received[length:]
|
||||
return result
|
||||
|
||||
def write(self, data):
|
||||
self.sent += data
|
||||
|
||||
def receive(self, data):
|
||||
self.received += data
|
||||
|
||||
def remaining(self):
|
||||
return len(self.received)
|
||||
|
||||
def flush(self):
|
||||
result = self.sent
|
||||
self.sent = ""
|
||||
return result
|
||||
|
||||
def read_varint(self):
|
||||
result = 0
|
||||
for i in range(5):
|
||||
part = ord(self.read(1))
|
||||
result |= (part & 0x7F) << 7 * i
|
||||
if not part & 0x80:
|
||||
return result
|
||||
raise IOError("Server sent a varint that was too big!")
|
||||
|
||||
def write_varint(self, value):
|
||||
remaining = value
|
||||
for i in range(5):
|
||||
if remaining & ~0x7F == 0:
|
||||
self.write(chr(remaining))
|
||||
return
|
||||
self.write(chr(remaining & 0x7F | 0x80))
|
||||
remaining >>= 7
|
||||
raise ValueError("The value %d is too big to send in a varint" % value)
|
||||
|
||||
def read_utf(self):
|
||||
length = self.read_varint()
|
||||
return str(self.read(length)).encode('utf8')
|
||||
|
||||
def write_utf(self, value):
|
||||
self.write_varint(len(value))
|
||||
self.write(bytearray(value.decode('utf8'), 'utf8'))
|
||||
|
||||
def read_short(self):
|
||||
return struct.unpack(">h", str(self.read(2)))[0]
|
||||
|
||||
def write_short(self, value):
|
||||
self.write(struct.pack(">h", value))
|
||||
|
||||
def read_ushort(self):
|
||||
return struct.unpack(">H", str(self.read(2)))[0]
|
||||
|
||||
def write_ushort(self, value):
|
||||
self.write(struct.pack(">H", value))
|
||||
|
||||
def read_long(self):
|
||||
return struct.unpack(">q", str(self.read(8)))[0]
|
||||
|
||||
def write_long(self, value):
|
||||
self.write(struct.pack(">q", value))
|
||||
|
||||
def read_ulong(self):
|
||||
return struct.unpack(">Q", str(self.read(8)))[0]
|
||||
|
||||
def write_ulong(self, value):
|
||||
self.write(struct.pack(">Q", value))
|
||||
|
||||
def read_buffer(self):
|
||||
length = self.read_varint()
|
||||
result = Connection()
|
||||
result.receive(self.read(length))
|
||||
return result
|
||||
|
||||
def write_buffer(self, buffer):
|
||||
data = buffer.flush()
|
||||
self.write_varint(len(data))
|
||||
self.write(data)
|
||||
|
||||
|
||||
class TCPSocketConnection(Connection):
|
||||
def __init__(self, addr):
|
||||
Connection.__init__(self)
|
||||
self.socket = socket.create_connection(addr, timeout=10)
|
||||
|
||||
def flush(self):
|
||||
raise TypeError("SocketConnection does not support flush()")
|
||||
|
||||
def receive(self, data):
|
||||
raise TypeError("SocketConnection does not support receive()")
|
||||
|
||||
def remaining(self):
|
||||
raise TypeError("SocketConnection does not support remaining()")
|
||||
|
||||
def read(self, length):
|
||||
result = ""
|
||||
while len(result) < length:
|
||||
result += self.socket.recv(length - len(result))
|
||||
return result
|
||||
|
||||
def write(self, data):
|
||||
self.socket.send(data)
|
||||
22
mcstatus/server.py
Normal file
22
mcstatus/server.py
Normal file
@@ -0,0 +1,22 @@
|
||||
import json
|
||||
import socket
|
||||
from mcstatus.pinger import ServerPinger
|
||||
from mcstatus.protocol.connection import TCPSocketConnection
|
||||
|
||||
|
||||
class MinecraftServer:
|
||||
def __init__(self, host, port=25565):
|
||||
self.host = host
|
||||
self.port = port
|
||||
|
||||
def ping_server(self, **kwargs):
|
||||
connection = TCPSocketConnection((self.host, self.port))
|
||||
pinger = ServerPinger(connection, host=self.host, port=self.port, **kwargs)
|
||||
pinger.handshake()
|
||||
try :
|
||||
return {
|
||||
"status": json.loads(pinger.read_status()),
|
||||
"latency": pinger.test_ping(),
|
||||
}
|
||||
except ValueError as ex:
|
||||
raise IOError("The server responded with invalid json")
|
||||
0
mcstatus/tests/__init__.py
Normal file
0
mcstatus/tests/__init__.py
Normal file
1
mcstatus/tests/protocol/__init__.py
Normal file
1
mcstatus/tests/protocol/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
__author__ = 'Dinnerbone'
|
||||
185
mcstatus/tests/protocol/test_connection.py
Normal file
185
mcstatus/tests/protocol/test_connection.py
Normal file
@@ -0,0 +1,185 @@
|
||||
import socket
|
||||
from unittest import TestCase
|
||||
from mock import Mock, patch
|
||||
|
||||
from mcstatus.protocol.connection import Connection, TCPSocketConnection
|
||||
|
||||
|
||||
class TestConnection(TestCase):
|
||||
def setUp(self):
|
||||
self.connection = Connection()
|
||||
|
||||
def test_flush(self):
|
||||
self.connection.sent = "\x7F\xAA\xBB"
|
||||
|
||||
self.assertEqual(self.connection.flush(), "\x7F\xAA\xBB")
|
||||
self.assertTrue(self.connection.sent == "")
|
||||
|
||||
def test_receive(self):
|
||||
self.connection.receive("\x7F")
|
||||
self.connection.receive("\xAA\xBB")
|
||||
|
||||
self.assertEqual(self.connection.received, "\x7F\xAA\xBB")
|
||||
|
||||
def test_remaining(self):
|
||||
self.connection.receive("\x7F")
|
||||
self.connection.receive("\xAA\xBB")
|
||||
|
||||
self.assertEqual(self.connection.remaining(), 3)
|
||||
|
||||
def test_send(self):
|
||||
self.connection.write("\x7F")
|
||||
self.connection.write("\xAA\xBB")
|
||||
|
||||
self.assertEqual(self.connection.flush(), "\x7F\xAA\xBB")
|
||||
|
||||
def test_read(self):
|
||||
self.connection.receive("\x7F\xAA\xBB")
|
||||
|
||||
self.assertEqual(self.connection.read(2), "\x7F\xAA")
|
||||
self.assertEqual(self.connection.read(1), "\xBB")
|
||||
|
||||
def test_readSimpleVarInt(self):
|
||||
self.connection.receive("\x0F")
|
||||
|
||||
self.assertEqual(self.connection.read_varint(), 15)
|
||||
|
||||
def test_writeSimpleVarInt(self):
|
||||
self.connection.write_varint(15)
|
||||
|
||||
self.assertEqual(self.connection.flush(), "\x0F")
|
||||
|
||||
def test_readBigVarInt(self):
|
||||
self.connection.receive("\xFF\xFF\xFF\xFF\x7F")
|
||||
|
||||
self.assertEqual(self.connection.read_varint(), 34359738367)
|
||||
|
||||
def test_writeBigVarInt(self):
|
||||
self.connection.write_varint(2147483647)
|
||||
|
||||
self.assertEqual(self.connection.flush(), "\xFF\xFF\xFF\xFF\x07")
|
||||
|
||||
def test_readInvalidVarInt(self):
|
||||
self.connection.receive("\xFF\xFF\xFF\xFF\x80")
|
||||
|
||||
self.assertRaises(IOError, self.connection.read_varint)
|
||||
|
||||
def test_writeInvalidVarInt(self):
|
||||
self.assertRaises(ValueError, self.connection.write_varint, 34359738368)
|
||||
|
||||
def test_readString(self):
|
||||
self.connection.receive("\x0D\x48\x65\x6C\x6C\x6F\x2C\x20\x77\x6F\x72\x6C\x64\x21")
|
||||
|
||||
self.assertEqual(self.connection.read_utf(), "Hello, world!")
|
||||
|
||||
def test_writeString(self):
|
||||
self.connection.write_utf("Hello, world!")
|
||||
|
||||
self.assertEqual(self.connection.flush(), "\x0D\x48\x65\x6C\x6C\x6F\x2C\x20\x77\x6F\x72\x6C\x64\x21")
|
||||
|
||||
def test_readEmptyString(self):
|
||||
self.connection.write_utf("")
|
||||
|
||||
self.assertEqual(self.connection.flush(), "\x00")
|
||||
|
||||
def test_readShortNegative(self):
|
||||
self.connection.receive("\x80\x00")
|
||||
|
||||
self.assertEqual(self.connection.read_short(), -32768)
|
||||
|
||||
def test_writeShortNegative(self):
|
||||
self.connection.write_short(-32768)
|
||||
|
||||
self.assertEqual(self.connection.flush(), "\x80\x00")
|
||||
|
||||
def test_readShortPositive(self):
|
||||
self.connection.receive("\x7F\xFF")
|
||||
|
||||
self.assertEqual(self.connection.read_short(), 32767)
|
||||
|
||||
def test_writeShortPositive(self):
|
||||
self.connection.write_short(32767)
|
||||
|
||||
self.assertEqual(self.connection.flush(), "\x7F\xFF")
|
||||
|
||||
def test_readUShortPositive(self):
|
||||
self.connection.receive("\x80\x00")
|
||||
|
||||
self.assertEqual(self.connection.read_ushort(), 32768)
|
||||
|
||||
def test_writeUShortPositive(self):
|
||||
self.connection.write_ushort(32768)
|
||||
|
||||
self.assertEqual(self.connection.flush(), "\x80\x00")
|
||||
|
||||
def test_readLongNegative(self):
|
||||
self.connection.receive("\x80\x00\x00\x00\x00\x00\x00\x00")
|
||||
|
||||
self.assertEqual(self.connection.read_long(), -9223372036854775808)
|
||||
|
||||
def test_writeLongNegative(self):
|
||||
self.connection.write_long(-9223372036854775808)
|
||||
|
||||
self.assertEqual(self.connection.flush(), "\x80\x00\x00\x00\x00\x00\x00\x00")
|
||||
|
||||
def test_readLongPositive(self):
|
||||
self.connection.receive("\x7F\xFF\xFF\xFF\xFF\xFF\xFF\xFF")
|
||||
|
||||
self.assertEqual(self.connection.read_long(), 9223372036854775807)
|
||||
|
||||
def test_writeLongPositive(self):
|
||||
self.connection.write_long(9223372036854775807)
|
||||
|
||||
self.assertEqual(self.connection.flush(), "\x7F\xFF\xFF\xFF\xFF\xFF\xFF\xFF")
|
||||
|
||||
def test_readULongPositive(self):
|
||||
self.connection.receive("\x80\x00\x00\x00\x00\x00\x00\x00")
|
||||
|
||||
self.assertEqual(self.connection.read_ulong(), 9223372036854775808)
|
||||
|
||||
def test_writeULongPositive(self):
|
||||
self.connection.write_ulong(9223372036854775808)
|
||||
|
||||
self.assertEqual(self.connection.flush(), "\x80\x00\x00\x00\x00\x00\x00\x00")
|
||||
|
||||
def test_readBuffer(self):
|
||||
self.connection.receive("\x02\x7F\xAA")
|
||||
buffer = self.connection.read_buffer()
|
||||
|
||||
self.assertEqual(buffer.received, "\x7F\xAA")
|
||||
self.assertEqual(self.connection.flush(), "")
|
||||
|
||||
def test_writeBuffer(self):
|
||||
buffer = Connection()
|
||||
buffer.write("\x7F\xAA")
|
||||
self.connection.write_buffer(buffer)
|
||||
|
||||
self.assertEqual(self.connection.flush(), "\x02\x7F\xAA")
|
||||
|
||||
class TCPSocketConnectionTest(TestCase):
|
||||
def setUp(self):
|
||||
socket = Mock()
|
||||
socket.recv = Mock()
|
||||
socket.send = Mock()
|
||||
with patch("socket.create_connection") as create_connection:
|
||||
create_connection.return_value = socket
|
||||
self.connection = TCPSocketConnection(("localhost", 1234))
|
||||
|
||||
def test_flush(self):
|
||||
self.assertRaises(TypeError, self.connection.flush)
|
||||
|
||||
def test_receive(self):
|
||||
self.assertRaises(TypeError, self.connection.receive, "")
|
||||
|
||||
def test_remaining(self):
|
||||
self.assertRaises(TypeError, self.connection.remaining)
|
||||
|
||||
def test_read(self):
|
||||
self.connection.socket.recv.return_value = "\x7F\xAA"
|
||||
|
||||
self.assertEqual(self.connection.read(2), "\x7F\xAA")
|
||||
|
||||
def test_write(self):
|
||||
self.connection.write("\x7F\xAA")
|
||||
|
||||
self.connection.socket.send.assert_called_once_with("\x7F\xAA")
|
||||
44
mcstatus/tests/protocol/test_pinger.py
Normal file
44
mcstatus/tests/protocol/test_pinger.py
Normal file
@@ -0,0 +1,44 @@
|
||||
from unittest import TestCase
|
||||
|
||||
from mcstatus.protocol.connection import Connection
|
||||
from mcstatus.pinger import ServerPinger
|
||||
|
||||
|
||||
class TestServerPinger(TestCase):
|
||||
def setUp(self):
|
||||
self.pinger = ServerPinger(Connection(), host="localhost", port=25565, version=44)
|
||||
|
||||
def test_handshake(self):
|
||||
self.pinger.handshake()
|
||||
|
||||
self.assertEqual(self.pinger.connection.flush(), "\x0F\x00\x2C\x09\x6C\x6F\x63\x61\x6C\x68\x6F\x73\x74\x63\xDD\x01")
|
||||
|
||||
def test_read_status(self):
|
||||
self.pinger.connection.receive("\x72\x00\x70\x7B\x22\x64\x65\x73\x63\x72\x69\x70\x74\x69\x6F\x6E\x22\x3A\x22\x41\x20\x4D\x69\x6E\x65\x63\x72\x61\x66\x74\x20\x53\x65\x72\x76\x65\x72\x22\x2C\x22\x70\x6C\x61\x79\x65\x72\x73\x22\x3A\x7B\x22\x6D\x61\x78\x22\x3A\x32\x30\x2C\x22\x6F\x6E\x6C\x69\x6E\x65\x22\x3A\x30\x7D\x2C\x22\x76\x65\x72\x73\x69\x6F\x6E\x22\x3A\x7B\x22\x6E\x61\x6D\x65\x22\x3A\x22\x31\x2E\x38\x2D\x70\x72\x65\x31\x22\x2C\x22\x70\x72\x6F\x74\x6F\x63\x6F\x6C\x22\x3A\x34\x34\x7D\x7D")
|
||||
|
||||
self.assertEqual(self.pinger.read_status(), '{"description":"A Minecraft Server","players":{"max":20,"online":0},"version":{"name":"1.8-pre1","protocol":44}}')
|
||||
self.assertEqual(self.pinger.connection.flush(), "\x01\x00")
|
||||
|
||||
def test_read_status_invalid(self):
|
||||
self.pinger.connection.receive("\x01\x05")
|
||||
|
||||
self.assertRaises(IOError, self.pinger.read_status)
|
||||
|
||||
def test_test_ping(self):
|
||||
self.pinger.connection.receive("\x09\x01\x00\x00\x00\x00\x00\xDD\x7D\x1C")
|
||||
self.pinger.ping_token = 14515484
|
||||
|
||||
self.assertTrue(self.pinger.test_ping() >= 0)
|
||||
self.assertEqual(self.pinger.connection.flush(), "\x09\x01\x00\x00\x00\x00\x00\xDD\x7D\x1C")
|
||||
|
||||
def test_test_ping_invalid(self):
|
||||
self.pinger.connection.receive("\x01\x1F")
|
||||
self.pinger.ping_token = 14515484
|
||||
|
||||
self.assertRaises(IOError, self.pinger.test_ping)
|
||||
|
||||
def test_test_ping_wrong_token(self):
|
||||
self.pinger.connection.receive("\x09\x01\x00\x00\x00\x00\x00\xDD\x7D\x1C")
|
||||
self.pinger.ping_token = 12345
|
||||
|
||||
self.assertRaises(IOError, self.pinger.test_ping)
|
||||
29
mcstatus/tests/test_server.py
Normal file
29
mcstatus/tests/test_server.py
Normal file
@@ -0,0 +1,29 @@
|
||||
from unittest import TestCase
|
||||
from mock import Mock, patch
|
||||
from mcstatus.protocol.connection import Connection
|
||||
from mcstatus.server import MinecraftServer
|
||||
|
||||
|
||||
class TestMinecraftServer(TestCase):
|
||||
def setUp(self):
|
||||
self.socket = Connection()
|
||||
self.server = MinecraftServer("localhost", port=25565)
|
||||
|
||||
def test_ping_server(self):
|
||||
self.socket.receive("\x6D\x00\x6B\x7B\x22\x64\x65\x73\x63\x72\x69\x70\x74\x69\x6F\x6E\x22\x3A\x22\x41\x20\x4D\x69\x6E\x65\x63\x72\x61\x66\x74\x20\x53\x65\x72\x76\x65\x72\x22\x2C\x22\x70\x6C\x61\x79\x65\x72\x73\x22\x3A\x7B\x22\x6D\x61\x78\x22\x3A\x32\x30\x2C\x22\x6F\x6E\x6C\x69\x6E\x65\x22\x3A\x30\x7D\x2C\x22\x76\x65\x72\x73\x69\x6F\x6E\x22\x3A\x7B\x22\x6E\x61\x6D\x65\x22\x3A\x22\x31\x2E\x38\x22\x2C\x22\x70\x72\x6F\x74\x6F\x63\x6F\x6C\x22\x3A\x34\x37\x7D\x7D\x09\x01\x00\x00\x00\x00\x01\xC5\x42\x46")
|
||||
|
||||
with patch("mcstatus.server.TCPSocketConnection") as connection:
|
||||
connection.return_value = self.socket
|
||||
info = self.server.ping_server(ping_token=29704774, version=47)
|
||||
|
||||
self.assertEqual(self.socket.flush(), "\x0F\x00\x2F\x09\x6C\x6F\x63\x61\x6C\x68\x6F\x73\x74\x63\xDD\x01\x01\x00\x09\x01\x00\x00\x00\x00\x01\xC5\x42\x46")
|
||||
self.assertEqual(self.socket.remaining(), 0, msg="Data is pending to be read, but should be empty")
|
||||
self.assertEqual(info["status"], {"description":"A Minecraft Server","players":{"max":20,"online":0},"version":{"name":"1.8","protocol":47}})
|
||||
self.assertTrue(info["latency"] >= 0)
|
||||
|
||||
def test_ping_server_invalid_json(self):
|
||||
self.socket.receive("\x6D\x00\x6B\x7C\x22\x64\x65\x73\x63\x72\x69\x70\x74\x69\x6F\x6E\x22\x3A\x22\x41\x20\x4D\x69\x6E\x65\x63\x72\x61\x66\x74\x20\x53\x65\x72\x76\x65\x72\x22\x2C\x22\x70\x6C\x61\x79\x65\x72\x73\x22\x3A\x7B\x22\x6D\x61\x78\x22\x3A\x32\x30\x2C\x22\x6F\x6E\x6C\x69\x6E\x65\x22\x3A\x30\x7D\x2C\x22\x76\x65\x72\x73\x69\x6F\x6E\x22\x3A\x7B\x22\x6E\x61\x6D\x65\x22\x3A\x22\x31\x2E\x38\x22\x2C\x22\x70\x72\x6F\x74\x6F\x63\x6F\x6C\x22\x3A\x34\x37\x7D\x7D\x09\x01\x00\x00\x00\x00\x01\xC5\x42\x46")
|
||||
|
||||
with patch("mcstatus.server.TCPSocketConnection") as connection:
|
||||
connection.return_value = self.socket
|
||||
self.assertRaises(IOError, self.server.ping_server, ping_token=29704774, version=47)
|
||||
5
setup.py
5
setup.py
@@ -2,10 +2,11 @@ from distutils.core import setup
|
||||
|
||||
setup(
|
||||
name='mcstatus',
|
||||
version='1.0',
|
||||
version='2.0dev',
|
||||
author='Nathan Adams',
|
||||
author_email='dinnerbone@dinnerbone.com',
|
||||
url='https://pypi.python.org/pypi/mcstatus',
|
||||
packages=['minecraft_query',],
|
||||
description='A library to query Minecraft Servers for their status and capabilities.'
|
||||
description='A library to query Minecraft Servers for their status and capabilities.',
|
||||
tests_require=['mock'],
|
||||
)
|
||||
Reference in New Issue
Block a user