diff --git a/minecraft/networking/connection.py b/minecraft/networking/connection.py index 0bf7c40..1e39398 100644 --- a/minecraft/networking/connection.py +++ b/minecraft/networking/connection.py @@ -5,19 +5,17 @@ import threading import socket import time import select -import authentication -from packets import * +import packets from types import VarInt -PROTOCOL_VERSION = 47 +from minecraft import PROTOCOL_VERSION class ConnectionOptions(object): # TODO: allow these options to be overriden from a constructor below address = None port = None - use_encryption = True compression_threshold = -1 compression_enabled = False @@ -40,8 +38,11 @@ class Connection(object): spawned = False def __init__(self, address, port, auth_token): - """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 + """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 :param address: address of the server to connect to :param port(int): port of the server to connect to @@ -58,10 +59,12 @@ class Connection(object): def write_packet(self, packet, force=False): """Writes a packet to the server. - 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' + + 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' :param packet: The :class:`network.packets.Packet` to write :param force(bool): Specifies if the packet write should be immediate @@ -84,14 +87,17 @@ class Connection(object): :param method: The method which will be called back with the packet :param args: The packets to listen for """ - self.packet_listeners.append(PacketListener(method, *args)) + self.packet_listeners.append(packets.PacketListener(method, *args)) def _pop_packet(self): - # Pops the topmost packet off the outgoing queue and writes it out through the socket + # Pops the topmost packet off the outgoing queue and writes it out + # through the socket # - # 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 + # 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 if len(self._outgoing_packet_queue) == 0: return False else: @@ -108,7 +114,7 @@ class Connection(object): self._start_network_thread() self.reactor = StatusReactor(self) - request_packet = RequestPacket() + request_packet = packets.RequestPacket() self.write_packet(request_packet) def connect(self): @@ -119,21 +125,23 @@ class Connection(object): self.reactor = LoginReactor(self) self._start_network_thread() - login_start_packet = LoginStartPacket() + login_start_packet = packets.LoginStartPacket() login_start_packet.name = self.auth_token.profile.name self.write_packet(login_start_packet) def _connect(self): - # 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 + # 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. self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.socket.connect((self.options.address, self.options.port)) self.file_object = self.socket.makefile() def _handshake(self, next_state=2): - handshake = HandShakePacket() + handshake = packets.HandShakePacket() handshake.protocol_version = PROTOCOL_VERSION handshake.server_address = self.options.address handshake.server_port = self.options.port @@ -155,7 +163,8 @@ class NetworkingThread(threading.Thread): while True: if self.interrupt: break - # Attempt to write out as many as 300 packets as possible every 0.05 seconds (20 ticks per second) + # Attempt to write out as many as 300 packets as possible every + # 0.05 seconds (20 ticks per second) num_packets = 0 self.connection._write_lock.acquire() while self.connection._pop_packet(): @@ -166,7 +175,8 @@ class NetworkingThread(threading.Thread): # Read and react to as many as 50 packets num_packets = 0 - packet = self.connection.reactor.read_packet(self.connection.file_object) + packet = self.connection.reactor.read_packet( + self.connection.file_object) while packet: num_packets += 1 @@ -176,7 +186,8 @@ class NetworkingThread(threading.Thread): if num_packets >= 50: break - packet = self.connection.reactor.read_packet(self.connection.file_object) + packet = self.connection.reactor.read_packet( + self.connection.file_object) time.sleep(0.05) @@ -194,23 +205,26 @@ class PacketReactor(object): self.connection = connection def read_packet(self, stream): - ready_to_read, _, __ = select.select([self.connection.socket], [], [], self.TIME_OUT) + ready_to_read, _, __ = select.select([self.connection.socket], [], [], + self.TIME_OUT) if self.connection.socket in ready_to_read: length = VarInt.read_socket(self.connection.socket) - packet_data = PacketBuffer() + packet_data = packets.PacketBuffer() 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: compressed_size = VarInt.read(packet_data) if compressed_size > 0: - decompressed_packet = decompress(packet_data.read(compressed_size)) + decompressed_packet = decompress( + packet_data.read(compressed_size)) packet_data.reset() packet_data.send(decompressed_packet) packet_data.reset_cursor() @@ -224,7 +238,7 @@ class PacketReactor(object): packet.read(packet_data) return packet else: - return Packet() + return packets.Packet() else: return None @@ -233,39 +247,39 @@ class PacketReactor(object): class LoginReactor(PacketReactor): - clientbound_packets = state_login_clientbound + clientbound_packets = packets.STATE_LOGIN_CLIENTBOUND def react(self, packet): - # TODO: Add some way to bypass encryption? (connection.options.use_encryption) Not sure if it's still possible. if packet.packet_name == "encryption request": import encryption secret = encryption.generate_shared_secret() - encrypted_token, encrypted_secret = encryption.encrypt_token_and_secret(packet.public_key, - packet.verify_token, secret) + token, encrypted_secret = encryption.encrypt_token_and_secret( + packet.public_key, packet.verify_token, secret) # A server id of '-' means the server is in offline mode if packet.server_id != '-': - server_id = encryption.generate_verification_hash(packet.server_id, secret, packet.public_key) - - self.connection.auth_token.join(server_id) - - encryption_response = EncryptionResponsePacket() - encryption_response.shared_secret = encrypted_secret - encryption_response.verify_token = encrypted_token + server_id = encryption.generate_verification_hash( + packet.server_id, secret, packet.public_key) - # Forced because we don't want to send this encrypted which it will be - # if we put it in the queue as we'd have wrapped the socket and file object by then + self.connection.auth_token.join(server_id) + + encryption_response = packets.EncryptionResponsePacket() + encryption_response.shared_secret = encrypted_secret + encryption_response.verify_token = token + + # Forced because we'll have encrypted the connection by the time + # it reaches the outgoing queue self.connection.write_packet(encryption_response, force=True) # Enable the encryption cipher = encryption.create_AES_cipher(secret) 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 = encryption.EncryptedSocketWrapper( + self.connection.socket, encryptor, decryptor) + self.connection.file_object = encryption.EncryptedFileObjectWrapper( + self.connection.file_object, decryptor) if packet.packet_name == "disconnect": print(packet.json_data) # TODO: handle propagating this back @@ -279,7 +293,7 @@ class LoginReactor(PacketReactor): class PlayingReactor(PacketReactor): - clientbound_packets = state_playing_clientbound + clientbound_packets = packets.STATE_PLAYING_CLIENTBOUND def react(self, packet): if packet.packet_name == "set compression": @@ -287,12 +301,12 @@ class PlayingReactor(PacketReactor): self.connection.options.compression_enabled = True if packet.packet_name == "keep alive": - keep_alive_packet = KeepAlivePacket() + keep_alive_packet = packets.KeepAlivePacket() keep_alive_packet.keep_alive_id = packet.keep_alive_id self.connection.write_packet(keep_alive_packet) if packet.packet_name == "player position and look": - position_response = PositionAndLookPacket() + position_response = packets.PositionAndLookPacket() position_response.x = packet.x position_response.feet_y = packet.y position_response.z = packet.z @@ -308,15 +322,15 @@ class PlayingReactor(PacketReactor): class StatusReactor(PacketReactor): - clientbound_packets = state_status_clientbound + clientbound_packets = packets.STATE_STATUS_CLIENTBOUND def react(self, packet): - if packet.id == ResponsePacket.id: + if packet.id == packets.ResponsePacket.id: import json print json.loads(packet.json_response) - ping_packet = PingPacket() + ping_packet = packets.PingPacket() ping_packet.time = int(time.time()) self.connection.write_packet(ping_packet) diff --git a/minecraft/networking/encryption.py b/minecraft/networking/encryption.py index fd21dbd..b53051a 100644 --- a/minecraft/networking/encryption.py +++ b/minecraft/networking/encryption.py @@ -12,12 +12,14 @@ def generate_shared_secret(): def create_AES_cipher(shared_secret): - cipher = Cipher(algorithms.AES(shared_secret), modes.CFB8(shared_secret), backend=default_backend()) + cipher = Cipher(algorithms.AES(shared_secret), modes.CFB8(shared_secret), + backend=default_backend()) return cipher def encrypt_token_and_secret(pubkey, verification_token, shared_secret): - """Encrypts the verification token and shared secret with the server's public key + """Encrypts the verification token and shared secret + with the server's public key. :param pubkey: The RSA public key provided by the server :param verification_token: The verification token provided by the server @@ -45,8 +47,8 @@ def generate_verification_hash(server_id, shared_secret, public_key): def minecraft_sha1_hash_digest(sha1_hash): - # Minecraft first parses the sha1 bytes as a signed number and then spits outs - # its hex representation + # Minecraft first parses the sha1 bytes as a signed number and then + # spits outs its hex representation number_representation = _number_from_bytes(sha1_hash.digest(), signed=True) return format(number_representation, 'x') @@ -82,4 +84,4 @@ class EncryptedSocketWrapper(object): self.actual_socket.send(self.encryptor.update(data)) def fileno(self): - return self.actual_socket.fileno() \ No newline at end of file + return self.actual_socket.fileno() diff --git a/minecraft/networking/packets.py b/minecraft/networking/packets.py index 44a0783..4ada358 100644 --- a/minecraft/networking/packets.py +++ b/minecraft/networking/packets.py @@ -102,10 +102,10 @@ class HandShakePacket(Packet): {'next_state': VarInt}] -state_handshake_clientbound = { +STATE_HANDSHAKE_CLIENTBOUND = { } -state_handshake_serverbound = { +STATE_HANDSHAKE_SERVERBOUND = { 0x00: HandShakePacket } @@ -125,7 +125,7 @@ class PingPacket(Packet): definition = [ {'time': Long}] -state_status_clientbound = { +STATE_STATUS_CLIENTBOUND = { 0x00: ResponsePacket, 0x01: PingPacket } @@ -143,7 +143,7 @@ class PingPacket(Packet): definition = [ {'time': Long}] -state_status_serverbound = { +STATE_STATUS_SERVERBOUND = { 0x00: RequestPacket, 0x01: PingPacket } @@ -181,7 +181,7 @@ class SetCompressionPacket(Packet): definition = [ {'threshold': VarInt}] -state_login_clientbound = { +STATE_LOGIN_CLIENTBOUND = { 0x00: DisconnectPacket, 0x01: EncryptionRequestPacket, 0x02: LoginSuccessPacket, @@ -203,7 +203,7 @@ class EncryptionResponsePacket(Packet): {'shared_secret': VarIntPrefixedByteArray}, {'verify_token': VarIntPrefixedByteArray}] -state_login_serverbound = { +STATE_LOGIN_SERVERBOUND = { 0x00: LoginStartPacket, 0x01: EncryptionResponsePacket } @@ -267,7 +267,7 @@ class SetCompressionPacketPlayState(Packet): {'threshold': VarInt}] -state_playing_clientbound = { +STATE_PLAYING_CLIENTBOUND = { 0x00: KeepAlivePacket, 0x01: JoinGamePacket, 0x02: ChatMessagePacket, @@ -295,7 +295,7 @@ class PositionAndLookPacket(Packet): {'pitch': Float}, {'on_ground': Boolean}] -state_playing_serverbound = { +STATE_PLAYING_SERVERBOUND = { 0x00: KeepAlivePacket, 0x01: ChatPacket, 0x06: PositionAndLookPacket diff --git a/tests/test_encryption.py b/tests/test_encryption.py index db6aef2..f8e00b6 100644 --- a/tests/test_encryption.py +++ b/tests/test_encryption.py @@ -4,14 +4,13 @@ from minecraft.networking.encryption import minecraft_sha1_hash_digest class Hashing(unittest.TestCase): - test_data = {'Notch': '4ed1f46bbe04bc756bcb17c0c7ce3e4632f06a48', - 'jeb_': '-7c9d5b0044c130109a5d7b5fb5c317c02b4e28c1', - 'simon': '88e16a1019277b15d58faf0541e11910eb756f6'} + 'jeb_': '-7c9d5b0044c130109a5d7b5fb5c317c02b4e28c1', + 'simon': '88e16a1019277b15d58faf0541e11910eb756f6'} def test_hashing(self): for input_value in self.test_data.iterkeys(): sha1_hash = hashlib.sha1() sha1_hash.update(input_value) - self.assertEquals(minecraft_sha1_hash_digest(sha1_hash), self.test_data[input_value]) - \ No newline at end of file + self.assertEquals(minecraft_sha1_hash_digest(sha1_hash), + self.test_data[input_value])