Added initial work for server pinging. Normal pings work, Query NYI.

This commit is contained in:
Nathan Adams
2014-09-04 22:25:21 +02:00
parent 34d0154f7a
commit f07976fbec
10 changed files with 453 additions and 2 deletions

54
mcstatus/pinger.py Normal file
View 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

View File

View 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
View 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")

View File

View File

@@ -0,0 +1 @@
__author__ = 'Dinnerbone'

View 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")

View 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)

View 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)

View File

@@ -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'],
)