pyCraft/tests/test_connection.py
Ammar Askar ca4fd6680e
Connect to localhost instead of the socket's binding address.
The bound address is 0.0.0.0 which usually implies all
available interfaces, which makes sense when listening
for something. However, when connecting to an address,
a specific address needs to be targeted. Hopefully, any
properly configured computer should have `localhost`
pointing to its loopback interface. Fixes #64
2017-07-16 00:19:30 -07:00

323 lines
11 KiB
Python

from __future__ import print_function
from minecraft import SUPPORTED_MINECRAFT_VERSIONS
from minecraft.networking import connection
from minecraft.networking import types
from minecraft.networking import packets
from future.utils import raise_
import unittest
import threading
import logging
import socket
import json
import sys
VERSIONS = sorted(SUPPORTED_MINECRAFT_VERSIONS.items(), key=lambda i: i[1])
THREAD_TIMEOUT_S = 5
class _ConnectTest(unittest.TestCase):
def _test_connect(self, client_version=None, server_version=None):
server = FakeServer(minecraft_version=server_version)
addr = "localhost"
port = server.listen_socket.getsockname()[1]
cond = threading.Condition()
def handle_client_exception(exc, exc_info):
with cond:
cond.exc_info = exc_info
cond.notify_all()
def client_write(packet, *args, **kwds):
def packet_write(*args, **kwds):
logging.debug('[C-> ] %s' % packet)
return real_packet_write(*args, **kwds)
real_packet_write = packet.write
packet.write = packet_write
return real_client_write(packet, *args, **kwds)
def client_react(packet, *args, **kwds):
logging.debug('[ ->C] %s' % packet)
return real_client_react(packet, *args, **kwds)
client = connection.Connection(
addr, port, username='User', initial_version=client_version,
handle_exception=handle_client_exception)
real_client_react = client._react
real_client_write = client.write_packet
client.write_packet = client_write
client._react = client_react
try:
with cond:
server_thread = threading.Thread(
name='_ConnectTest server',
target=self._test_connect_server,
args=(server, cond))
server_thread.daemon = True
server_thread.start()
self._test_connect_client(client, cond)
cond.exc_info = Ellipsis
cond.wait(THREAD_TIMEOUT_S)
if cond.exc_info is Ellipsis:
self.fail('Timed out.')
elif cond.exc_info is not None:
raise_(*cond.exc_info)
finally:
# Wait for all threads to exit.
for thread in server_thread, client.networking_thread:
if thread is not None and thread.is_alive():
thread.join(THREAD_TIMEOUT_S)
if thread is not None and thread.is_alive():
if cond.exc_info is None:
self.fail('Thread "%s" timed out.' % thread.name)
else:
# Keep the earlier exception, if there is one.
break
def _test_connect_client(self, client, cond):
client.connect()
def _test_connect_server(self, server, cond):
try:
server.run()
exc_info = None
except:
exc_info = sys.exc_info()
with cond:
cond.exc_info = exc_info
cond.notify_all()
class ConnectOldToOldTest(_ConnectTest):
def runTest(self):
self._test_connect(VERSIONS[0][1], VERSIONS[0][0])
class ConnectOldToNewTest(_ConnectTest):
def runTest(self):
self._test_connect(VERSIONS[0][1], VERSIONS[-1][0])
class ConnectNewToOldTest(_ConnectTest):
def runTest(self):
self._test_connect(VERSIONS[-1][1], VERSIONS[0][0])
class ConnectNewToNewTest(_ConnectTest):
def runTest(self):
self._test_connect(VERSIONS[-1][1], VERSIONS[-1][0])
class PingTest(_ConnectTest):
def runTest(self):
self._test_connect()
def _test_connect_client(self, client, cond):
def handle_ping(latency_ms):
assert 0 <= latency_ms < 60000
with cond:
cond.exc_info = None
cond.notify_all()
client.status(handle_status=False, handle_ping=handle_ping)
def _test_connect_server(self, server, cond):
try:
server.continue_after_status = False
server.run()
except:
with cond:
cond.exc_info = sys.exc_info()
cond.notify_all()
class FakeServer(threading.Thread):
__slots__ = 'context', 'minecraft_version', 'listen_socket', \
'packets_login', 'packets_playing', 'packets_status', \
'packets',
def __init__(self, minecraft_version=None, continue_after_status=True):
if minecraft_version is None:
minecraft_version = VERSIONS[-1][0]
self.minecraft_version = minecraft_version
self.continue_after_status = continue_after_status
protocol_version = SUPPORTED_MINECRAFT_VERSIONS[minecraft_version]
self.context = connection.ConnectionContext(
protocol_version=protocol_version)
self.packets_handshake = {
p.get_id(self.context): p for p in
packets.state_handshake_serverbound(self.context)}
self.packets_login = {
p.get_id(self.context): p for p in
packets.state_login_serverbound(self.context)}
self.packets_playing = {
p.get_id(self.context): p for p in
packets.state_playing_serverbound(self.context)}
self.packets_status = {
p.get_id(self.context): p for p in
packets.state_status_serverbound(self.context)}
self.listen_socket = socket.socket()
self.listen_socket.bind(('0.0.0.0', 0))
self.listen_socket.listen(0)
super(FakeServer, self).__init__()
def run(self):
try:
self.run_accept()
finally:
self.listen_socket.close()
def run_accept(self):
running = True
while running:
client_socket, addr = self.listen_socket.accept()
logging.debug('[ ++ ] Client %s connected to server.' % (addr,))
client_file = client_socket.makefile('rb', 0)
try:
running = self.run_handshake(client_socket, client_file)
except:
raise
else:
client_socket.shutdown(socket.SHUT_RDWR)
logging.debug('[ -- ] Client %s disconnected.' % (addr,))
finally:
client_socket.close()
client_file.close()
def run_handshake(self, client_socket, client_file):
self.packets = self.packets_handshake
packet = self.read_packet_filtered(client_file)
assert isinstance(packet, packets.HandShakePacket)
if packet.next_state == 1:
return self.run_handshake_status(
packet, client_socket, client_file)
elif packet.next_state == 2:
return self.run_handshake_play(
packet, client_socket, client_file)
else:
raise AssertionError('Unknown state: %s' % packet.next_state)
def run_handshake_status(self, packet, client_socket, client_file):
self.run_status(client_socket, client_file)
return self.continue_after_status
def run_handshake_play(self, packet, client_socket, client_file):
if packet.protocol_version == self.context.protocol_version:
self.run_login(client_socket, client_file)
else:
if packet.protocol_version < self.context.protocol_version:
msg = 'Outdated client! Please use %s' \
% self.minecraft_version
else:
msg = "Outdated server! I'm still on %s" \
% self.minecraft_version
packet = packets.DisconnectPacket(
self.context, json_data=json.dumps({'text': msg}))
self.write_packet(packet, client_socket)
return True
def run_login(self, client_socket, client_file):
self.packets = self.packets_login
packet = self.read_packet_filtered(client_file)
assert isinstance(packet, packets.LoginStartPacket)
packet = packets.LoginSuccessPacket(
self.context, UUID='{fake uuid}', Username=packet.name)
self.write_packet(packet, client_socket)
self.run_playing(client_socket, client_file)
def run_playing(self, client_socket, client_file):
self.packets = self.packets_playing
packet = packets.JoinGamePacket(
self.context, entity_id=0, game_mode=0, dimension=0, difficulty=2,
max_players=1, level_type='default', reduced_debug_info=False)
self.write_packet(packet, client_socket)
keep_alive_id = 1076048782
packet = packets.KeepAlivePacketClientbound(
self.context, keep_alive_id=keep_alive_id)
self.write_packet(packet, client_socket)
packet = self.read_packet_filtered(client_file)
assert isinstance(packet, packets.KeepAlivePacketServerbound)
assert packet.keep_alive_id == keep_alive_id
packet = packets.DisconnectPacketPlayState(
self.context, json_data=json.dumps({'text': 'Test complete.'}))
self.write_packet(packet, client_socket)
return False
def run_status(self, client_socket, client_file):
self.packets = self.packets_status
packet = self.read_packet(client_file)
assert isinstance(packet, packets.RequestPacket)
packet = packets.ResponsePacket(self.context)
packet.json_response = json.dumps({
'version': {
'name': self.minecraft_version,
'protocol': self.context.protocol_version},
'players': {
'max': 1,
'online': 0,
'sample': []},
'description': {
'text': 'FakeServer'}})
self.write_packet(packet, client_socket)
try:
packet = self.read_packet(client_file)
except EOFError:
return False
assert isinstance(packet, packets.PingPacket)
res_packet = packets.PingPacketResponse(self.context)
res_packet.time = packet.time
self.write_packet(res_packet, client_socket)
return False
def read_packet_filtered(self, client_file):
while True:
packet = self.read_packet(client_file)
if isinstance(packet, packets.PositionAndLookPacket):
continue
if isinstance(packet, packets.AnimationPacketServerbound):
continue
return packet
def read_packet(self, client_file):
buffer = self.read_packet_buffer(client_file)
packet_id = types.VarInt.read(buffer)
if packet_id in self.packets:
packet = self.packets[packet_id](self.context)
packet.read(buffer)
else:
packet = packets.Packet(self.context, id=packet_id)
logging.debug('[ ->S] %s' % packet)
return packet
def read_packet_buffer(self, client_file):
length = types.VarInt.read(client_file)
buffer = packets.PacketBuffer()
while len(buffer.get_writable()) < length:
buffer.send(client_file.read(length - len(buffer.get_writable())))
buffer.reset_cursor()
return buffer
def write_packet(self, packet, client_socket):
packet.write(client_socket)
logging.debug('[S-> ] %s' % packet)