mirror of
https://github.com/ammaraskar/pyCraft.git
synced 2024-11-16 07:15:24 +01:00
ca4fd6680e
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
323 lines
11 KiB
Python
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)
|