diff --git a/minecraft/networking/connection.py b/minecraft/networking/connection.py index 5713295..98b712c 100644 --- a/minecraft/networking/connection.py +++ b/minecraft/networking/connection.py @@ -14,9 +14,7 @@ from .packets import clientbound, serverbound from . import packets from . import encryption from .. import SUPPORTED_PROTOCOL_VERSIONS, SUPPORTED_MINECRAFT_VERSIONS -from ..exceptions import ( - VersionMismatch, LoginDisconnect, IgnorePacket, InvalidState -) +from ..exceptions import VersionMismatch, LoginDisconnect, IgnorePacket, InvalidState STATE_STATUS = 1 @@ -28,13 +26,19 @@ class ConnectionContext(object): 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') + self.protocol_version = kwds.get("protocol_version") class _ConnectionOptions(object): - def __init__(self, address=None, port=None, compression_threshold=-1, - compression_enabled=False): + def __init__( + self, + address=None, + port=None, + compression_threshold=-1, + compression_enabled=False, + ): self.address = address self.port = port self.compression_threshold = compression_threshold @@ -46,6 +50,7 @@ class Connection(object): server, it handles everything from connecting, sending packets to handling default network behaviour """ + def __init__( self, address, @@ -63,41 +68,51 @@ class Connection(object): The connect method needs to be called in order to actually begin the connection - :param address: address of the server to connect to - :param port(int): port of the server to connect to - :param auth_token: :class:`minecraft.authentication.AuthenticationToken` - object. 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 ID string or protocol - version number to use if the server's protocol - version cannot be determined. (Although it is - now somewhat inaccurate, this name is retained - for backward compatibility.) - :param allowed_versions: A set of versions, each being a Minecraft - version ID string or protocol version number, - restricting the versions that the client may - use in connecting to the server. - :param handle_exception: The final exception handler. This is triggered - when an exception occurs in the networking - thread that is not caught normally. After - any other user-registered exception handlers - are run, the final exception (which may be the - original exception or one raised by another - handler) is passed, regardless of whether or - not it was caught by another handler, to the - final handler, which may be a function obeying - the protocol of 'register_exception_handler'; - the value 'None', meaning that if the - exception was otherwise uncaught, it is - re-raised from the networking thread after - closing the connection; or the value 'False', - meaning that the exception is never re-raised. - :param handle_exit: A function to be called when a connection to a - server terminates, not caused by an exception, - and not with the intention to automatically - reconnect. Exceptions raised from this function - will be handled by any matching exception handlers. + Parameters + ---------- + address + address of the server to connect to + port : int + port of the server to connect to + auth_token : `minecraft.authentication.AuthenticationToken` + If None, no authentication is attempted and + the server is assumed to be running in offline mode. + username : str + Username string; only applicable in offline mode. + initial_version + A Minecraft version ID string or protocol + version number to use if the server's protocol + version cannot be determined. (Although it is + now somewhat inaccurate, this name is retained + for backward compatibility.) + allowed_versions + A set of versions, each being a Minecraft + version ID string or protocol version number, + restricting the versions that the client may + use in connecting to the server. + handle_exception + The final exception handler. This is triggered + when an exception occurs in the networking + thread that is not caught normally. After + any other user-registered exception handlers + are run, the final exception (which may be the + original exception or one raised by another + handler) is passed, regardless of whether or + not it was caught by another handler, to the + final handler, which may be a function obeying + the protocol of 'register_exception_handler'; + the value 'None', meaning that if the + exception was otherwise uncaught, it is + re-raised from the networking thread after + closing the connection; or the value 'False', + meaning that the exception is never re-raised. + handle_exit + A function to be called when a connection to a + server terminates, not caused by an exception, + and not with the intention to automatically + reconnect. Exceptions raised from this function + will be handled by any matching exception handlers. + """ # NOQA # This lock is re-entrant because it may be acquired in a re-entrant @@ -120,7 +135,7 @@ class Connection(object): else: proto_version = None if proto_version not in SUPPORTED_PROTOCOL_VERSIONS: - raise ValueError('Unsupported version number: %r.' % version) + raise ValueError("Unsupported version number: %r." % version) return proto_version if allowed_versions is None: @@ -135,7 +150,8 @@ class Connection(object): self.default_proto_version = proto_version(initial_version) self.context = ConnectionContext( - protocol_version=max(self.allowed_proto_versions)) + protocol_version=max(self.allowed_proto_versions) + ) self.options = _ConnectionOptions() self.options.address = address @@ -154,10 +170,12 @@ class Connection(object): def _start_network_thread(self): with self._write_lock: - if self.networking_thread is not None and \ - not self.networking_thread.interrupt or \ - self.new_networking_thread is not None: - raise InvalidState('A networking thread is already running.') + if ( + self.networking_thread is not None + and not self.networking_thread.interrupt + or self.new_networking_thread is not None + ): + raise InvalidState("A networking thread is already running.") elif self.networking_thread is None: self.networking_thread = NetworkingThread(self) self.networking_thread.start() @@ -165,8 +183,9 @@ class Connection(object): # This thread will wait until the existing thread exits, and # then set 'networking_thread' to itself and # 'new_networking_thread' to None. - self.new_networking_thread \ - = NetworkingThread(self, previous=self.networking_thread) + self.new_networking_thread = NetworkingThread( + self, previous=self.networking_thread + ) self.new_networking_thread.start() def write_packet(self, packet, force=False): @@ -178,8 +197,13 @@ class Connection(object): 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' - :param packet: The :class:`network.packets.Packet` to write - :param force(bool): Specifies if the packet write should be immediate + Parameters + ---------- + packet : network.packets.Packet + The `network.packets.Packet` to write + force : bool + Specifies if the packet write should be immediate + """ packet.context = self.context if force: @@ -189,13 +213,19 @@ class Connection(object): self._outgoing_packet_queue.append(packet) def listener(self, *packet_types, **kwds): - """ - Shorthand decorator to register a function as a packet listener. + """Shorthand decorator to register a function as a packet listener. Wraps :meth:`minecraft.networking.connection.register_packet_listener` - :param packet_types: Packet types to listen for. - :param kwds: Keyword arguments for `register_packet_listener` + + Parameters + ---------- + packet_types + Packet types to listen for. + kwds + Keyword arguments for `register_packet_listener` + """ + def listener_decorator(handler_func): self.register_packet_listener(handler_func, *packet_types, **kwds) return handler_func @@ -206,6 +236,7 @@ class Connection(object): """ Shorthand decorator to register a function as an exception handler. """ + def exception_handler_decorator(handler_func): self.register_exception_handler(handler_func, *exc_types, **kwds) return handler_func @@ -213,8 +244,7 @@ class Connection(object): return exception_handler_decorator def register_packet_listener(self, method, *packet_types, **kwds): - """ - Registers a listener method which will be notified when a packet of + """Registers a listener method which will be notified when a packet of a selected type is received. If :class:`minecraft.networking.connection.IgnorePacket` is raised from @@ -225,23 +255,38 @@ class Connection(object): 'outgoing=True', this will prevent the packet from being written to the network. - :param method: The method which will be called back with the packet - :param packet_types: The packets to listen for - :param outgoing: If 'True', this listener will be called on outgoing - packets just after they are sent to the server, rather - than on incoming packets. - :param early: If 'True', this listener will be called before any - built-in default action is carried out, and before any - listeners with 'early=False' are called. If - 'outgoing=True', the listener will be called before the - packet is written to the network, rather than afterwards. + Parameters + ---------- + method + The method which will be called back with the packet + packet_types + The packets to listen for + outgoing + If 'True', this listener will be called on outgoing + packets just after they are sent to the server, rather + than on incoming packets. + early + If 'True', this listener will be called before any + built-in default action is carried out, and before any + listeners with 'early=False' are called. If + 'outgoing=True', the listener will be called before the + packet is written to the network, rather than afterwards. + + Returns + ------- + """ - outgoing = kwds.pop('outgoing', False) - early = kwds.pop('early', False) - target = self.packet_listeners if not early and not outgoing \ - else self.early_packet_listeners if early and not outgoing \ - else self.outgoing_packet_listeners if not early \ + outgoing = kwds.pop("outgoing", False) + early = kwds.pop("early", False) + target = ( + self.packet_listeners + if not early and not outgoing + else self.early_packet_listeners + if early and not outgoing + else self.outgoing_packet_listeners + if not early else self.early_outgoing_packet_listeners + ) target.append(packets.PacketListener(method, *packet_types, **kwds)) def register_exception_handler(self, handler_func, *exc_types, **kwds): @@ -262,21 +307,24 @@ class Connection(object): be set as the 'exception' and 'exc_info' attributes of the 'Connection'. - :param handler_func: A function taking two arguments: the exception - object 'e' as in 'except Exception as e:', and the corresponding - 3-tuple given by 'sys.exc_info()'. The return value of the function is - ignored, but any exception raised in it replaces the original - exception, and may be passed to later exception handlers. - - :param exc_types: The types of exceptions that this handler shall - catch, as in 'except (exc_type_1, exc_type_2, ...) as e:'. If this is - empty, the handler will catch all exceptions. - - :param early: If 'True', the exception handler is registered before - any existing exception handlers in the handling order. + Parameters + ---------- + handler_func + A function taking two arguments: the exception + object 'e' as in 'except Exception as e:', and the corresponding + 3-tuple given by 'sys.exc_info()'. The return value of the function is + ignored, but any exception raised in it replaces the original + exception, and may be passed to later exception handlers. + exc_types + The types of exceptions that this handler shall + catch, as in 'except (exc_type_1, exc_type_2, ...) as e:'. If this is + empty, the handler will catch all exceptions. + early + If 'True', the exception handler is registered before + any existing exception handlers in the handling order. """ - early = kwds.pop('early', False) - assert not kwds, 'Unexpected keyword arguments: %r' % (kwds,) + early = kwds.pop("early", False) + assert not kwds, "Unexpected keyword arguments: %r" % (kwds,) if early: self._exception_handlers.insert(0, (handler_func, exc_types)) else: @@ -317,14 +365,19 @@ class Connection(object): 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. + Parameters + ---------- + 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. + 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. + """ with self._write_lock: # pylint: disable=not-context-manager self._check_connection() @@ -387,10 +440,12 @@ class Connection(object): self._start_network_thread() def _check_connection(self): - if self.networking_thread is not None and \ - not self.networking_thread.interrupt or \ - self.new_networking_thread is not None: - raise InvalidState('There is an existing connection.') + if ( + self.networking_thread is not None + and not self.networking_thread.interrupt + or self.new_networking_thread is not None + ): + raise InvalidState("There is an existing connection.") def _connect(self): # Connect a socket to the server and create a file object from the @@ -401,15 +456,18 @@ class Connection(object): # the server. self._outgoing_packet_queue = deque() - info = socket.getaddrinfo(self.options.address, self.options.port, - 0, socket.SOCK_STREAM) + info = socket.getaddrinfo( + self.options.address, self.options.port, 0, socket.SOCK_STREAM + ) # Prefer to use IPv4 (for backward compatibility with previous # versions that always resolved hostnames to IPv4 addresses), # then IPv6, then other address families. def key(ai): - return 0 if ai[0] == socket.AF_INET else \ - 1 if ai[0] == socket.AF_INET6 else 2 + return ( + 0 if ai[0] == socket.AF_INET else 1 if ai[0] == socket.AF_INET6 else 2 + ) + ai_faml, ai_type, ai_prot, _ai_cnam, ai_addr = min(info, key=key) self.socket = socket.socket(ai_faml, ai_type, ai_prot) @@ -421,7 +479,14 @@ class Connection(object): def disconnect(self, immediate=False): """Terminate the existing server connection, if there is one. - If 'immediate' is True, do not attempt to write any packets. + + If 'immediate' is True, do not attempt to write any packets. + + Parameters + ---------- + immediate : bool, optional + Whether or not to terminate the existing connection immediately + """ with self._write_lock: # pylint: disable=not-context-manager self.connected = False @@ -499,14 +564,20 @@ class Connection(object): server_protocol = SUPPORTED_MINECRAFT_VERSIONS.get(server_version) if server_protocol is None: - vs = 'version' if server_version is None else \ - ('version of %s' % server_version) + vs = ( + "version" + if server_version is None + else ("version of %s" % server_version) + ) else: - vs = ('protocol version of %d' % server_protocol) + \ - ('' if server_version is None else ' (%s)' % server_version) - ss = 'supported, but not allowed for this connection' \ - if server_protocol in SUPPORTED_PROTOCOL_VERSIONS \ - else 'not supported' + vs = ("protocol version of %d" % server_protocol) + ( + "" if server_version is None else " (%s)" % server_version + ) + ss = ( + "supported, but not allowed for this connection" + if server_protocol in SUPPORTED_PROTOCOL_VERSIONS + else "not supported" + ) err = VersionMismatch("Server's %s is %s." % (vs, ss)) err.server_protocol = server_protocol err.server_version = server_version @@ -579,7 +650,8 @@ class NetworkingThread(threading.Thread): # Read and react to as many as 50 packets. while num_packets < 50 and not self.interrupt: packet = self.connection.reactor.read_packet( - self.connection.file_object, timeout=read_timeout) + self.connection.file_object, timeout=read_timeout + ) if not packet: break num_packets += 1 @@ -601,6 +673,7 @@ class PacketReactor(object): """ Reads and reacts to packets """ + state_name = None # Handshaking is considered the "default" state @@ -611,7 +684,8 @@ class PacketReactor(object): context = self.connection.context self.clientbound_packets = { packet.get_id(context): packet - for packet in self.__class__.get_clientbound_packets(context)} + for packet in self.__class__.get_clientbound_packets(context) + } def read_packet(self, stream, timeout=0): # Block for up to `timeout' seconds waiting for `stream' to become @@ -625,19 +699,18 @@ class PacketReactor(object): packet_data.send(stream.read(length)) # Ensure we read all the packet while len(packet_data.get_writable()) < length: - packet_data.send( - stream.read(length - len(packet_data.get_writable()))) + packet_data.send(stream.read(length - len(packet_data.get_writable()))) packet_data.reset_cursor() if self.connection.options.compression_enabled: decompressed_size = VarInt.read(packet_data) if decompressed_size > 0: decompressor = zlib.decompressobj() - decompressed_packet = decompressor.decompress( - packet_data.read()) - assert len(decompressed_packet) == decompressed_size, \ - 'decompressed length %d, but expected %d' % \ - (len(decompressed_packet), decompressed_size) + decompressed_packet = decompressor.decompress(packet_data.read()) + assert len(decompressed_packet) == decompressed_size, ( + "decompressed length %d, but expected %d" + % (len(decompressed_packet), decompressed_size) + ) packet_data.reset() packet_data.send(decompressed_packet) packet_data.reset_cursor() @@ -682,12 +755,14 @@ class LoginReactor(PacketReactor): secret = encryption.generate_shared_secret() token, encrypted_secret = encryption.encrypt_token_and_secret( - packet.public_key, packet.verify_token, secret) + packet.public_key, packet.verify_token, secret + ) # A server id of '-' means the server is in offline mode - if packet.server_id != '-': + if packet.server_id != "-": server_id = encryption.generate_verification_hash( - packet.server_id, secret, packet.public_key) + packet.server_id, secret, packet.public_key + ) if self.connection.auth_token is not None: self.connection.auth_token.join(server_id) @@ -704,25 +779,29 @@ class LoginReactor(PacketReactor): encryptor = cipher.encryptor() decryptor = cipher.decryptor() self.connection.socket = encryption.EncryptedSocketWrapper( - self.connection.socket, encryptor, decryptor) - self.connection.file_object = \ - encryption.EncryptedFileObjectWrapper( - self.connection.file_object, decryptor) + self.connection.socket, encryptor, decryptor + ) + self.connection.file_object = encryption.EncryptedFileObjectWrapper( + self.connection.file_object, decryptor + ) elif packet.packet_name == "disconnect": # Receiving a disconnect packet in the login state indicates an # abnormal condition. Raise an exception explaining the situation. try: - msg = json.loads(packet.json_data)['text'] + msg = json.loads(packet.json_data)["text"] except (ValueError, TypeError, KeyError): msg = packet.json_data - match = re.match(r"Outdated (client! Please use|server!" - r" I'm still on) (?P\S+)$", msg) + match = re.match( + r"Outdated (client! Please use|server!" r" I'm still on) (?P\S+)$", + msg, + ) if match: - ver = match.group('ver') + ver = match.group("ver") self.connection._version_mismatch(server_version=ver) - raise LoginDisconnect('The server rejected our login attempt ' - 'with: "%s".' % msg) + raise LoginDisconnect( + "The server rejected our login attempt " 'with: "%s".' % msg + ) elif packet.packet_name == "login success": self.connection.reactor = PlayingReactor(self.connection) @@ -734,7 +813,9 @@ class LoginReactor(PacketReactor): elif packet.packet_name == "login plugin request": self.connection.write_packet( serverbound.login.PluginResponsePacket( - message_id=packet.message_id, successful=False)) + message_id=packet.message_id, successful=False + ) + ) class PlayingReactor(PacketReactor): @@ -800,7 +881,7 @@ class StatusReactor(PacketReactor): print(status_dict) def handle_ping(self, latency_ms): - print('Ping: %d ms' % latency_ms) + print("Ping: %d ms" % latency_ms) class PlayingStatusReactor(StatusReactor): @@ -812,15 +893,15 @@ class PlayingStatusReactor(StatusReactor): # This can occur when we connect to a Mojang server while it is # still initialising, so it must not cause the client to connect # with the default version. - raise IOError('Invalid server status.') - elif 'version' not in status or 'protocol' not in status['version']: + raise IOError("Invalid server status.") + elif "version" not in status or "protocol" not in status["version"]: return self.handle_failure() - proto = status['version']['protocol'] + proto = status["version"]["protocol"] if proto not in self.connection.allowed_proto_versions: self.connection._version_mismatch( - server_protocol=proto, - server_version=status['version'].get('name')) + server_protocol=proto, server_version=status["version"].get("name") + ) self.handle_proto_version(proto)