2016-03-07 03:40:25 +01:00
|
|
|
from __future__ import print_function
|
|
|
|
|
2014-06-07 01:47:34 +02:00
|
|
|
from collections import deque
|
|
|
|
from threading import Lock
|
2015-03-17 18:15:27 +01:00
|
|
|
from zlib import decompress
|
2014-06-07 01:47:34 +02:00
|
|
|
import threading
|
|
|
|
import socket
|
|
|
|
import time
|
2016-11-20 06:03:23 +01:00
|
|
|
import timeit
|
2014-06-07 01:47:34 +02:00
|
|
|
import select
|
2015-10-07 06:51:40 +02:00
|
|
|
import sys
|
2016-03-07 03:40:25 +01:00
|
|
|
import json
|
|
|
|
import re
|
2014-06-07 01:47:34 +02:00
|
|
|
|
2016-06-17 14:54:24 +02:00
|
|
|
from future.utils import raise_
|
|
|
|
|
2016-03-07 03:40:25 +01:00
|
|
|
from ..compat import unicode
|
2015-04-03 00:13:22 +02:00
|
|
|
from .types import VarInt
|
|
|
|
from . import packets
|
2015-04-03 19:04:45 +02:00
|
|
|
from . import encryption
|
2016-03-05 08:28:14 +01:00
|
|
|
from .. import SUPPORTED_PROTOCOL_VERSIONS
|
2016-03-07 03:40:25 +01:00
|
|
|
from .. import SUPPORTED_MINECRAFT_VERSIONS
|
|
|
|
|
2016-06-18 19:22:18 +02:00
|
|
|
|
2016-03-07 03:40:25 +01:00
|
|
|
class ConnectionContext(object):
|
|
|
|
"""A ConnectionContext encapsulates the static configuration parameters
|
|
|
|
shared by the Connection class with other classes, such as Packet.
|
|
|
|
Importantly, it can be used without knowing the interface of Connection.
|
|
|
|
"""
|
|
|
|
def __init__(self, **kwds):
|
|
|
|
self.protocol_version = kwds.get('protocol_version')
|
2015-03-17 18:15:27 +01:00
|
|
|
|
2016-06-18 19:22:18 +02:00
|
|
|
|
2015-04-03 00:13:22 +02:00
|
|
|
class _ConnectionOptions(object):
|
2016-06-18 19:22:18 +02:00
|
|
|
def __init__(self, address=None, port=None, compression_threshold=-1,
|
|
|
|
compression_enabled=False):
|
2015-10-07 06:51:40 +02:00
|
|
|
self.address = address
|
|
|
|
self.port = port
|
|
|
|
self.compression_threshold = compression_threshold
|
|
|
|
self.compression_enabled = compression_enabled
|
2015-03-17 18:15:27 +01:00
|
|
|
|
2016-06-18 19:22:18 +02:00
|
|
|
|
2015-03-17 18:15:27 +01:00
|
|
|
class Connection(object):
|
2014-06-07 01:47:34 +02:00
|
|
|
"""This class represents a connection to a minecraft
|
2015-03-17 18:15:27 +01:00
|
|
|
server, it handles everything from connecting, sending packets to
|
2014-06-07 01:47:34 +02:00
|
|
|
handling default network behaviour
|
|
|
|
"""
|
2016-03-05 08:28:14 +01:00
|
|
|
def __init__(
|
|
|
|
self,
|
|
|
|
address,
|
2016-11-20 06:03:23 +01:00
|
|
|
port=25565,
|
2016-06-18 19:22:18 +02:00
|
|
|
auth_token=None,
|
|
|
|
username=None,
|
|
|
|
initial_version=None,
|
|
|
|
allowed_versions=None,
|
2016-03-05 08:28:14 +01:00
|
|
|
):
|
2015-04-02 22:44:03 +02:00
|
|
|
"""Sets up an instance of this object to be able to connect to a
|
|
|
|
minecraft server.
|
|
|
|
|
|
|
|
The connect method needs to be called in order to actually begin
|
|
|
|
the connection
|
2015-03-17 18:15:27 +01:00
|
|
|
|
|
|
|
:param address: address of the server to connect to
|
|
|
|
:param port(int): port of the server to connect to
|
2015-04-01 23:38:10 +02:00
|
|
|
:param auth_token: :class:`authentication.AuthenticationToken` object.
|
2016-06-18 19:22:18 +02:00
|
|
|
If None, no authentication is attempted and the
|
|
|
|
server is assumed to be running in offline mode.
|
|
|
|
:param username: Username string; only applicable in offline mode.
|
|
|
|
:param initial_version: A Minecraft version string or protocol version
|
|
|
|
number to use as a first guess when connecting
|
|
|
|
to the server.
|
|
|
|
:param allowed_versions: A set of versions, each being a Minecraft
|
|
|
|
version string or protocol version number,
|
|
|
|
restricting the versions that the client may
|
|
|
|
use in connecting to the server.
|
2015-03-17 18:15:27 +01:00
|
|
|
"""
|
2015-10-07 06:51:40 +02:00
|
|
|
|
|
|
|
self._write_lock = Lock()
|
|
|
|
self.networking_thread = None
|
|
|
|
self.packet_listeners = []
|
2016-03-07 03:40:25 +01:00
|
|
|
|
|
|
|
def proto_version(version):
|
|
|
|
if isinstance(version, str):
|
|
|
|
proto_version = SUPPORTED_MINECRAFT_VERSIONS.get(version)
|
|
|
|
elif isinstance(version, int):
|
|
|
|
proto_version = version
|
|
|
|
else:
|
|
|
|
proto_version = None
|
|
|
|
if proto_version not in SUPPORTED_PROTOCOL_VERSIONS:
|
|
|
|
raise ValueError('Unsupported version number: %r.' % version)
|
|
|
|
return proto_version
|
|
|
|
|
|
|
|
if allowed_versions is None:
|
2016-10-01 16:52:17 +02:00
|
|
|
self.allowed_proto_versions = set(SUPPORTED_PROTOCOL_VERSIONS)
|
2016-03-07 03:40:25 +01:00
|
|
|
else:
|
2016-06-18 19:22:18 +02:00
|
|
|
allowed_version = set(map(proto_version, allowed_versions))
|
|
|
|
self.allowed_proto_versions = allowed_version
|
2016-03-07 03:40:25 +01:00
|
|
|
|
|
|
|
if initial_version is None:
|
|
|
|
initial_proto_version = max(self.allowed_proto_versions)
|
|
|
|
else:
|
|
|
|
initial_proto_version = proto_version(initial_version)
|
|
|
|
self.context = ConnectionContext(
|
|
|
|
protocol_version=initial_proto_version)
|
2015-10-07 06:51:40 +02:00
|
|
|
|
|
|
|
self.options = _ConnectionOptions()
|
2015-03-17 18:15:27 +01:00
|
|
|
self.options.address = address
|
|
|
|
self.options.port = port
|
2015-04-01 23:38:10 +02:00
|
|
|
self.auth_token = auth_token
|
2016-06-18 19:22:18 +02:00
|
|
|
self.username = username
|
2015-10-07 06:51:40 +02:00
|
|
|
|
|
|
|
# The reactor handles all the default responses to packets,
|
|
|
|
# it should be changed per networking state
|
2015-03-17 18:15:27 +01:00
|
|
|
self.reactor = PacketReactor(self)
|
2014-06-07 01:47:34 +02:00
|
|
|
|
|
|
|
def _start_network_thread(self):
|
2016-03-07 03:40:25 +01:00
|
|
|
"""May safely be called multiple times."""
|
2016-11-20 06:03:23 +01:00
|
|
|
if self.networking_thread is not None:
|
|
|
|
if not self.networking_thread.interrupt:
|
|
|
|
return
|
|
|
|
self.networking_thread.join()
|
|
|
|
self.networking_thread = NetworkingThread(self)
|
|
|
|
self.networking_thread.start()
|
2014-06-07 01:47:34 +02:00
|
|
|
|
|
|
|
def write_packet(self, packet, force=False):
|
2015-03-17 18:15:27 +01:00
|
|
|
"""Writes a packet to the server.
|
2015-04-02 22:44:03 +02:00
|
|
|
|
|
|
|
If force is set to true, the method attempts to acquire the write lock
|
|
|
|
and write the packet out immediately, and as such may block.
|
|
|
|
|
|
|
|
If force is false then the packet will be added to the end of the
|
|
|
|
packet writing queue to be sent 'as soon as possible'
|
2015-03-17 18:15:27 +01:00
|
|
|
|
2015-03-21 23:08:44 +01:00
|
|
|
:param packet: The :class:`network.packets.Packet` to write
|
2015-03-17 18:15:27 +01:00
|
|
|
:param force(bool): Specifies if the packet write should be immediate
|
|
|
|
"""
|
2016-03-05 08:28:14 +01:00
|
|
|
packet.context = self.context
|
2014-06-07 01:47:34 +02:00
|
|
|
if force:
|
2015-03-17 18:15:27 +01:00
|
|
|
self._write_lock.acquire()
|
|
|
|
if self.options.compression_enabled:
|
|
|
|
packet.write(self.socket, self.options.compression_threshold)
|
|
|
|
else:
|
|
|
|
packet.write(self.socket)
|
|
|
|
self._write_lock.release()
|
2014-06-07 01:47:34 +02:00
|
|
|
else:
|
2015-03-17 18:15:27 +01:00
|
|
|
self._outgoing_packet_queue.append(packet)
|
2014-06-07 01:47:34 +02:00
|
|
|
|
2015-03-22 15:05:55 +01:00
|
|
|
def register_packet_listener(self, method, *args):
|
|
|
|
"""
|
|
|
|
Registers a listener method which will be notified when a packet of
|
|
|
|
a selected type is received
|
|
|
|
|
|
|
|
:param method: The method which will be called back with the packet
|
|
|
|
:param args: The packets to listen for
|
|
|
|
"""
|
2015-04-02 22:44:03 +02:00
|
|
|
self.packet_listeners.append(packets.PacketListener(method, *args))
|
2015-03-22 15:05:55 +01:00
|
|
|
|
2014-06-07 01:47:34 +02:00
|
|
|
def _pop_packet(self):
|
2015-04-02 22:44:03 +02:00
|
|
|
# Pops the topmost packet off the outgoing queue and writes it out
|
|
|
|
# through the socket
|
2015-03-17 18:15:27 +01:00
|
|
|
#
|
2015-04-02 22:44:03 +02:00
|
|
|
# Mostly an internal convenience function, caller should make sure
|
|
|
|
# they have the write lock acquired to avoid issues caused by
|
|
|
|
# asynchronous access to the socket.
|
|
|
|
# This should be the only method that removes elements from the
|
|
|
|
# outbound queue
|
2015-03-17 18:15:27 +01:00
|
|
|
if len(self._outgoing_packet_queue) == 0:
|
2014-06-07 01:47:34 +02:00
|
|
|
return False
|
|
|
|
else:
|
2015-03-17 18:15:27 +01:00
|
|
|
packet = self._outgoing_packet_queue.popleft()
|
|
|
|
if self.options.compression_enabled:
|
|
|
|
packet.write(self.socket, self.options.compression_threshold)
|
|
|
|
else:
|
|
|
|
packet.write(self.socket)
|
2014-06-07 01:47:34 +02:00
|
|
|
return True
|
|
|
|
|
2016-11-20 06:03:23 +01:00
|
|
|
def status(self, handle_status=None, handle_ping=False):
|
|
|
|
"""Issue a status request to the server and then disconnect.
|
|
|
|
|
|
|
|
:param handle_status: a function to be called with the status
|
|
|
|
dictionary None for the default behaviour of
|
|
|
|
printing the dictionary to standard output, or
|
|
|
|
False to ignore the result.
|
|
|
|
:param handle_ping: a function to be called with the measured latency
|
|
|
|
in milliseconds, None for the default handler,
|
|
|
|
which prints the latency to standard outout, or
|
|
|
|
False, to prevent measurement of the latency.
|
|
|
|
"""
|
2014-06-07 01:47:34 +02:00
|
|
|
self._connect()
|
|
|
|
self._handshake(1)
|
|
|
|
self._start_network_thread()
|
2016-11-20 06:03:23 +01:00
|
|
|
|
|
|
|
self.reactor = StatusReactor(self, do_ping=handle_ping is not False)
|
|
|
|
|
|
|
|
if handle_status is False:
|
|
|
|
self.reactor.handle_status = lambda *args, **kwds: None
|
|
|
|
elif handle_status is not None:
|
|
|
|
self.reactor.handle_status = handle_status
|
|
|
|
|
|
|
|
if handle_ping is False:
|
|
|
|
self.reactor.handle_ping = lambda *args, **kwds: None
|
|
|
|
elif handle_ping is not None:
|
|
|
|
self.reactor.handle_ping = handle_ping
|
2014-06-07 01:47:34 +02:00
|
|
|
|
2015-04-02 22:44:03 +02:00
|
|
|
request_packet = packets.RequestPacket()
|
2014-06-07 01:47:34 +02:00
|
|
|
self.write_packet(request_packet)
|
|
|
|
|
|
|
|
def connect(self):
|
2015-03-17 18:15:27 +01:00
|
|
|
"""
|
2016-03-07 03:40:25 +01:00
|
|
|
Attempt to begin connecting to the server.
|
|
|
|
May safely be called multiple times after the first, i.e. to reconnect.
|
|
|
|
"""
|
|
|
|
with self._write_lock:
|
2016-09-02 01:26:12 +02:00
|
|
|
# We hold the lock throughout, as connect() may be called by both
|
|
|
|
# the network thread and a parent thread simultaneously, during
|
|
|
|
# automatic version negotiation.
|
|
|
|
|
|
|
|
self.spawned = False
|
|
|
|
self._connect()
|
|
|
|
self._handshake()
|
|
|
|
login_start_packet = packets.LoginStartPacket()
|
|
|
|
if self.auth_token:
|
|
|
|
login_start_packet.name = self.auth_token.profile.name
|
|
|
|
else:
|
|
|
|
login_start_packet.name = self.username
|
|
|
|
self.write_packet(login_start_packet)
|
2014-06-07 01:47:34 +02:00
|
|
|
|
2016-09-02 01:26:12 +02:00
|
|
|
self.reactor = LoginReactor(self)
|
|
|
|
self._start_network_thread()
|
2015-03-17 18:15:27 +01:00
|
|
|
|
2014-06-07 01:47:34 +02:00
|
|
|
def _connect(self):
|
2015-04-02 22:44:03 +02:00
|
|
|
# Connect a socket to the server and create a file object from the
|
|
|
|
# socket.
|
|
|
|
# The file object is used to read any and all data from the socket
|
|
|
|
# since it's "guaranteed" to read the number of bytes specified,
|
|
|
|
# the socket itself will mostly be used to write data upstream to
|
|
|
|
# the server.
|
2016-11-20 06:03:23 +01:00
|
|
|
self._outgoing_packet_queue = deque()
|
2014-06-07 01:47:34 +02:00
|
|
|
self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
2015-03-17 18:15:27 +01:00
|
|
|
self.socket.connect((self.options.address, self.options.port))
|
2016-06-18 19:22:18 +02:00
|
|
|
self.file_object = self.socket.makefile("rb", 0)
|
2014-06-07 01:47:34 +02:00
|
|
|
|
2016-11-20 06:03:23 +01:00
|
|
|
def disconnect(self):
|
|
|
|
""" Terminate the existing server connection, if there is one. """
|
|
|
|
if self.networking_thread is None:
|
|
|
|
self._disconnect()
|
|
|
|
else:
|
|
|
|
# The networking thread will call _disconnect() later.
|
|
|
|
self.networking_thread.interrupt = True
|
|
|
|
|
|
|
|
def _disconnect(self):
|
|
|
|
if self.socket is not None:
|
|
|
|
if hasattr(self.socket, 'actual_socket'):
|
|
|
|
# pylint: disable=no-member
|
|
|
|
actual_socket = self.socket.actual_socket
|
|
|
|
else:
|
|
|
|
actual_socket = self.socket
|
|
|
|
|
|
|
|
try:
|
|
|
|
actual_socket.shutdown(socket.SHUT_RDWR)
|
|
|
|
finally:
|
|
|
|
actual_socket.close()
|
|
|
|
self.socket = None
|
|
|
|
|
2014-06-07 01:47:34 +02:00
|
|
|
def _handshake(self, next_state=2):
|
2015-04-02 22:44:03 +02:00
|
|
|
handshake = packets.HandShakePacket()
|
2016-03-05 08:28:14 +01:00
|
|
|
handshake.protocol_version = self.context.protocol_version
|
2015-03-17 18:15:27 +01:00
|
|
|
handshake.server_address = self.options.address
|
|
|
|
handshake.server_port = self.options.port
|
2014-06-07 01:47:34 +02:00
|
|
|
handshake.next_state = next_state
|
|
|
|
|
2014-11-10 16:25:30 +01:00
|
|
|
self.write_packet(handshake)
|
2014-06-07 01:47:34 +02:00
|
|
|
|
2016-06-18 19:22:18 +02:00
|
|
|
|
2014-06-07 01:47:34 +02:00
|
|
|
class NetworkingThread(threading.Thread):
|
|
|
|
def __init__(self, connection):
|
|
|
|
threading.Thread.__init__(self)
|
2015-10-07 06:51:40 +02:00
|
|
|
self.interrupt = False
|
2014-06-07 01:47:34 +02:00
|
|
|
self.connection = connection
|
|
|
|
self.name = "Networking Thread"
|
|
|
|
self.daemon = True
|
|
|
|
|
|
|
|
def run(self):
|
2015-10-05 04:14:48 +02:00
|
|
|
try:
|
|
|
|
self._run()
|
2016-11-20 06:03:23 +01:00
|
|
|
except Exception as e:
|
|
|
|
e.exc_info = sys.exc_info()
|
|
|
|
self.connection.exception = e
|
|
|
|
finally:
|
|
|
|
self.connection.networking_thread = None
|
2015-10-05 04:14:48 +02:00
|
|
|
|
|
|
|
def _run(self):
|
2014-06-07 01:47:34 +02:00
|
|
|
while True:
|
|
|
|
if self.interrupt:
|
2016-11-20 06:03:23 +01:00
|
|
|
self.connection._disconnect()
|
2014-06-07 01:47:34 +02:00
|
|
|
break
|
2016-03-28 06:08:01 +02:00
|
|
|
|
2015-04-02 22:44:03 +02:00
|
|
|
# Attempt to write out as many as 300 packets as possible every
|
|
|
|
# 0.05 seconds (20 ticks per second)
|
2014-06-07 01:47:34 +02:00
|
|
|
num_packets = 0
|
2015-03-17 18:15:27 +01:00
|
|
|
self.connection._write_lock.acquire()
|
2016-03-28 06:08:01 +02:00
|
|
|
try:
|
|
|
|
while self.connection._pop_packet():
|
|
|
|
num_packets += 1
|
|
|
|
if num_packets >= 300:
|
|
|
|
break
|
|
|
|
exc_info = None
|
|
|
|
except:
|
|
|
|
exc_info = sys.exc_info()
|
2015-03-17 18:15:27 +01:00
|
|
|
self.connection._write_lock.release()
|
2014-06-07 01:47:34 +02:00
|
|
|
|
2015-04-02 23:25:34 +02:00
|
|
|
# Read and react to as many as 50 packets
|
2014-06-07 01:47:34 +02:00
|
|
|
num_packets = 0
|
2015-04-02 22:44:03 +02:00
|
|
|
packet = self.connection.reactor.read_packet(
|
|
|
|
self.connection.file_object)
|
2014-06-07 01:47:34 +02:00
|
|
|
while packet:
|
|
|
|
num_packets += 1
|
2016-06-18 19:22:18 +02:00
|
|
|
|
|
|
|
# Do not raise an IOError if it occurred while a disconnect
|
|
|
|
# packet was received, as this may be part of an orderly
|
|
|
|
# disconnection.
|
|
|
|
if packet.packet_name == 'disconnect' and \
|
|
|
|
exc_info is not None and \
|
|
|
|
isinstance(exc_info[1], IOError):
|
|
|
|
exc_info = None
|
|
|
|
|
2016-03-07 03:40:25 +01:00
|
|
|
try:
|
|
|
|
self.connection.reactor.react(packet)
|
|
|
|
for listener in self.connection.packet_listeners:
|
|
|
|
listener.call_packet(packet)
|
|
|
|
except IgnorePacket:
|
|
|
|
pass
|
2015-03-22 15:05:55 +01:00
|
|
|
|
2014-06-07 01:47:34 +02:00
|
|
|
if num_packets >= 50:
|
|
|
|
break
|
2016-06-18 19:22:18 +02:00
|
|
|
|
2016-11-20 06:03:23 +01:00
|
|
|
if self.interrupt:
|
|
|
|
self.connection._disconnect()
|
|
|
|
break
|
|
|
|
|
2015-04-02 22:44:03 +02:00
|
|
|
packet = self.connection.reactor.read_packet(
|
|
|
|
self.connection.file_object)
|
2014-06-07 01:47:34 +02:00
|
|
|
|
2016-03-28 06:08:01 +02:00
|
|
|
if exc_info is not None:
|
2016-06-17 14:54:24 +02:00
|
|
|
raise_(*exc_info)
|
2016-03-28 06:08:01 +02:00
|
|
|
|
2014-06-07 01:47:34 +02:00
|
|
|
time.sleep(0.05)
|
|
|
|
|
|
|
|
|
2016-03-07 03:40:25 +01:00
|
|
|
class IgnorePacket(Exception):
|
|
|
|
"""
|
|
|
|
This exception may be raised from within a packet handler, such as
|
|
|
|
`PacketReactor.react' or a packet listener added with
|
|
|
|
`Connection.register_packet_listener', to stop any subsequent handlers from
|
|
|
|
being called on that particular packet.
|
|
|
|
"""
|
|
|
|
pass
|
|
|
|
|
2016-06-18 19:22:18 +02:00
|
|
|
|
2015-03-17 18:15:27 +01:00
|
|
|
class PacketReactor(object):
|
2015-03-22 15:05:55 +01:00
|
|
|
"""
|
|
|
|
Reads and reacts to packets
|
|
|
|
"""
|
2014-06-07 01:47:34 +02:00
|
|
|
state_name = None
|
2015-09-12 17:41:13 +02:00
|
|
|
TIME_OUT = 0
|
2014-06-07 01:47:34 +02:00
|
|
|
|
2016-03-05 08:28:14 +01:00
|
|
|
get_clientbound_packets = staticmethod(lambda context: set())
|
|
|
|
|
2014-06-07 01:47:34 +02:00
|
|
|
def __init__(self, connection):
|
|
|
|
self.connection = connection
|
2016-03-05 08:28:14 +01:00
|
|
|
context = self.connection.context
|
|
|
|
self.clientbound_packets = {
|
|
|
|
packet.get_id(context): packet
|
|
|
|
for packet in self.__class__.get_clientbound_packets(context)}
|
2014-06-07 01:47:34 +02:00
|
|
|
|
2014-10-08 19:12:37 +02:00
|
|
|
def read_packet(self, stream):
|
2016-06-17 14:54:24 +02:00
|
|
|
ready_to_read = select.select([stream], [], [], self.TIME_OUT)[0]
|
2015-03-17 18:15:27 +01:00
|
|
|
|
2016-06-18 19:22:18 +02:00
|
|
|
if ready_to_read:
|
2016-06-17 14:54:24 +02:00
|
|
|
length = VarInt.read(stream)
|
2014-06-07 01:47:34 +02:00
|
|
|
|
2015-04-02 22:44:03 +02:00
|
|
|
packet_data = packets.PacketBuffer()
|
2015-03-22 13:39:15 +01:00
|
|
|
packet_data.send(stream.read(length))
|
|
|
|
# Ensure we read all the packet
|
|
|
|
while len(packet_data.get_writable()) < length:
|
2015-04-02 22:44:03 +02:00
|
|
|
packet_data.send(
|
|
|
|
stream.read(length - len(packet_data.get_writable())))
|
2015-03-22 13:39:15 +01:00
|
|
|
packet_data.reset_cursor()
|
|
|
|
|
2015-03-17 18:15:27 +01:00
|
|
|
if self.connection.options.compression_enabled:
|
2016-09-27 12:32:39 +02:00
|
|
|
decompressed_size = VarInt.read(packet_data)
|
|
|
|
if decompressed_size > 0:
|
|
|
|
decompressed_packet = decompress(packet_data.read())
|
|
|
|
assert len(decompressed_packet) == decompressed_size, \
|
|
|
|
'decompressed length %d, but expected %d' % \
|
|
|
|
(len(decompressed_packet), decompressed_size)
|
2015-03-22 13:39:15 +01:00
|
|
|
packet_data.reset()
|
|
|
|
packet_data.send(decompressed_packet)
|
|
|
|
packet_data.reset_cursor()
|
2015-03-17 18:15:27 +01:00
|
|
|
|
2015-03-22 13:39:15 +01:00
|
|
|
packet_id = VarInt.read(packet_data)
|
2015-03-17 18:15:27 +01:00
|
|
|
|
|
|
|
# If we know the structure of the packet, attempt to parse it
|
|
|
|
# otherwise just skip it
|
2014-06-07 01:47:34 +02:00
|
|
|
if packet_id in self.clientbound_packets:
|
|
|
|
packet = self.clientbound_packets[packet_id]()
|
2016-03-05 08:28:14 +01:00
|
|
|
packet.context = self.connection.context
|
2015-03-22 13:39:15 +01:00
|
|
|
packet.read(packet_data)
|
2014-06-07 01:47:34 +02:00
|
|
|
return packet
|
|
|
|
else:
|
2016-03-05 08:28:14 +01:00
|
|
|
return packets.Packet(context=self.connection.context)
|
2014-06-07 01:47:34 +02:00
|
|
|
else:
|
|
|
|
return None
|
|
|
|
|
|
|
|
def react(self, packet):
|
2015-03-17 18:15:27 +01:00
|
|
|
raise NotImplementedError("Call to base reactor")
|
|
|
|
|
|
|
|
|
|
|
|
class LoginReactor(PacketReactor):
|
2016-03-05 08:28:14 +01:00
|
|
|
get_clientbound_packets = staticmethod(packets.state_login_clientbound)
|
2015-03-17 18:15:27 +01:00
|
|
|
|
|
|
|
def react(self, packet):
|
|
|
|
if packet.packet_name == "encryption request":
|
|
|
|
|
|
|
|
secret = encryption.generate_shared_secret()
|
2015-04-02 22:44:03 +02:00
|
|
|
token, encrypted_secret = encryption.encrypt_token_and_secret(
|
|
|
|
packet.public_key, packet.verify_token, secret)
|
2015-03-17 18:15:27 +01:00
|
|
|
|
|
|
|
# A server id of '-' means the server is in offline mode
|
|
|
|
if packet.server_id != '-':
|
2015-04-02 22:44:03 +02:00
|
|
|
server_id = encryption.generate_verification_hash(
|
|
|
|
packet.server_id, secret, packet.public_key)
|
2016-06-18 19:22:18 +02:00
|
|
|
if self.connection.auth_token is not None:
|
|
|
|
self.connection.auth_token.join(server_id)
|
2015-04-02 22:44:03 +02:00
|
|
|
|
|
|
|
encryption_response = packets.EncryptionResponsePacket()
|
2015-03-17 18:15:27 +01:00
|
|
|
encryption_response.shared_secret = encrypted_secret
|
2015-04-02 22:44:03 +02:00
|
|
|
encryption_response.verify_token = token
|
2014-06-07 01:47:34 +02:00
|
|
|
|
2015-04-02 22:44:03 +02:00
|
|
|
# Forced because we'll have encrypted the connection by the time
|
|
|
|
# it reaches the outgoing queue
|
2015-03-17 18:15:27 +01:00
|
|
|
self.connection.write_packet(encryption_response, force=True)
|
|
|
|
|
|
|
|
# Enable the encryption
|
|
|
|
cipher = encryption.create_AES_cipher(secret)
|
|
|
|
encryptor = cipher.encryptor()
|
|
|
|
decryptor = cipher.decryptor()
|
2015-04-02 22:44:03 +02:00
|
|
|
self.connection.socket = encryption.EncryptedSocketWrapper(
|
|
|
|
self.connection.socket, encryptor, decryptor)
|
2015-04-02 23:25:34 +02:00
|
|
|
self.connection.file_object = \
|
|
|
|
encryption.EncryptedFileObjectWrapper(
|
|
|
|
self.connection.file_object, decryptor)
|
2015-03-17 18:15:27 +01:00
|
|
|
|
|
|
|
if packet.packet_name == "disconnect":
|
2016-03-07 03:40:25 +01:00
|
|
|
# Test for a disconnect packet indicating a version mismatch.
|
|
|
|
# (Note: it is known how the disconnect messages are formatted for
|
|
|
|
# official servers within SUPPORTED_MINECRAFT_VERSIONS, but in case
|
|
|
|
# new versions are added, this section may need to be updated.)
|
2016-06-18 19:22:18 +02:00
|
|
|
try:
|
|
|
|
data = json.loads(packet.json_data)
|
|
|
|
except ValueError:
|
|
|
|
pass
|
2016-03-07 03:40:25 +01:00
|
|
|
if isinstance(data, dict) and 'text' in data:
|
|
|
|
data = data['text']
|
2016-06-18 19:22:18 +02:00
|
|
|
if not isinstance(data, (str, unicode)):
|
|
|
|
return
|
2016-03-07 03:40:25 +01:00
|
|
|
match = re.match(
|
|
|
|
r"(Outdated client! Please use"
|
|
|
|
r"|Outdated server! I'm still on) (?P<version>.*)", data)
|
2016-06-18 19:22:18 +02:00
|
|
|
if not match:
|
|
|
|
return
|
2016-10-01 16:52:17 +02:00
|
|
|
|
|
|
|
self.connection.allowed_proto_versions.remove(
|
|
|
|
self.connection.context.protocol_version)
|
|
|
|
|
2016-03-07 03:40:25 +01:00
|
|
|
version = match.group('version')
|
|
|
|
if version in SUPPORTED_MINECRAFT_VERSIONS:
|
|
|
|
new_version = SUPPORTED_MINECRAFT_VERSIONS[version]
|
|
|
|
elif data.startswith('Outdated client!'):
|
2016-06-18 19:22:18 +02:00
|
|
|
new_version = max(SUPPORTED_PROTOCOL_VERSIONS)
|
2016-03-07 03:40:25 +01:00
|
|
|
elif data.startswith('Outdated server!'):
|
|
|
|
new_version = min(SUPPORTED_PROTOCOL_VERSIONS)
|
2016-10-01 16:52:17 +02:00
|
|
|
if new_version in self.connection.allowed_proto_versions:
|
2016-06-18 19:22:18 +02:00
|
|
|
# Ignore this disconnect packet and reconnect with the new
|
|
|
|
# protocol version, making it appear (on the client side) as if
|
|
|
|
# the client had initially connected with the (hopefully)
|
|
|
|
# correct version.
|
2016-03-07 03:40:25 +01:00
|
|
|
self.connection.context.protocol_version = new_version
|
|
|
|
self.connection.connect()
|
|
|
|
raise IgnorePacket
|
2015-03-17 18:15:27 +01:00
|
|
|
|
|
|
|
if packet.packet_name == "login success":
|
|
|
|
self.connection.reactor = PlayingReactor(self.connection)
|
|
|
|
|
|
|
|
if packet.packet_name == "set compression":
|
|
|
|
self.connection.options.compression_threshold = packet.threshold
|
|
|
|
self.connection.options.compression_enabled = True
|
|
|
|
|
2016-06-18 19:22:18 +02:00
|
|
|
|
2015-03-17 18:15:27 +01:00
|
|
|
class PlayingReactor(PacketReactor):
|
2016-03-05 08:28:14 +01:00
|
|
|
get_clientbound_packets = staticmethod(packets.state_playing_clientbound)
|
2015-03-17 18:15:27 +01:00
|
|
|
|
|
|
|
def react(self, packet):
|
|
|
|
if packet.packet_name == "set compression":
|
|
|
|
self.connection.options.compression_threshold = packet.threshold
|
|
|
|
self.connection.options.compression_enabled = True
|
2014-06-07 01:47:34 +02:00
|
|
|
|
2015-03-22 13:39:15 +01:00
|
|
|
if packet.packet_name == "keep alive":
|
2016-03-07 07:22:42 +01:00
|
|
|
keep_alive_packet = packets.KeepAlivePacketServerbound()
|
2015-03-22 13:39:15 +01:00
|
|
|
keep_alive_packet.keep_alive_id = packet.keep_alive_id
|
|
|
|
self.connection.write_packet(keep_alive_packet)
|
|
|
|
|
2015-03-22 14:16:47 +01:00
|
|
|
if packet.packet_name == "player position and look":
|
2016-03-28 06:08:01 +02:00
|
|
|
teleport_confirm = packets.TeleportConfirmPacket()
|
|
|
|
teleport_confirm.teleport_id = packet.teleport_id
|
|
|
|
self.connection.write_packet(teleport_confirm)
|
|
|
|
'''
|
2015-04-02 22:44:03 +02:00
|
|
|
position_response = packets.PositionAndLookPacket()
|
2015-03-22 14:16:47 +01:00
|
|
|
position_response.x = packet.x
|
|
|
|
position_response.feet_y = packet.y
|
|
|
|
position_response.z = packet.z
|
|
|
|
position_response.yaw = packet.yaw
|
|
|
|
position_response.pitch = packet.pitch
|
|
|
|
position_response.on_ground = True
|
|
|
|
self.connection.write_packet(position_response)
|
2016-03-28 06:08:01 +02:00
|
|
|
'''
|
2015-03-22 14:20:01 +01:00
|
|
|
self.connection.spawned = True
|
2015-03-22 14:16:47 +01:00
|
|
|
|
|
|
|
if packet.packet_name == "disconnect":
|
2016-11-20 06:03:23 +01:00
|
|
|
self.connection.disconnect()
|
2014-06-07 01:47:34 +02:00
|
|
|
|
2016-06-18 19:22:18 +02:00
|
|
|
|
2014-06-07 01:47:34 +02:00
|
|
|
class StatusReactor(PacketReactor):
|
2016-03-05 08:28:14 +01:00
|
|
|
get_clientbound_packets = staticmethod(packets.state_status_clientbound)
|
2014-06-07 01:47:34 +02:00
|
|
|
|
2016-11-20 06:03:23 +01:00
|
|
|
def __init__(self, connection, do_ping=False):
|
|
|
|
super(StatusReactor, self).__init__(connection)
|
|
|
|
self.do_ping = do_ping
|
|
|
|
|
2014-06-07 01:47:34 +02:00
|
|
|
def react(self, packet):
|
2016-03-07 03:40:25 +01:00
|
|
|
if packet.packet_name == "response":
|
2016-11-20 06:03:23 +01:00
|
|
|
if self.do_ping:
|
|
|
|
ping_packet = packets.PingPacket()
|
|
|
|
# NOTE: it may be better to depend on the `monotonic' package
|
|
|
|
# or something similar for more accurate time measurement.
|
|
|
|
ping_packet.time = int(1000 * timeit.default_timer())
|
|
|
|
self.connection.write_packet(ping_packet)
|
|
|
|
else:
|
|
|
|
self.connection.disconnect()
|
|
|
|
self.handle_status(json.loads(packet.json_response))
|
|
|
|
|
|
|
|
elif packet.packet_name == "ping" and self.do_ping:
|
|
|
|
now = int(1000 * timeit.default_timer())
|
|
|
|
self.connection.disconnect()
|
|
|
|
self.handle_ping(now - packet.time)
|
2014-06-07 01:47:34 +02:00
|
|
|
|
2016-11-20 06:03:23 +01:00
|
|
|
def handle_status(self, status_dict):
|
|
|
|
print(status_dict)
|
2014-06-07 01:47:34 +02:00
|
|
|
|
2016-11-20 06:03:23 +01:00
|
|
|
def handle_ping(self, latency_ms):
|
|
|
|
print('Ping: %d ms' % latency_ms)
|