def test_file_object_wrapper(self): cipher = create_AES_cipher(generate_shared_secret()) encryptor = cipher.encryptor() decryptor = cipher.decryptor() test_data = "hello".encode('utf-8') io = BytesIO() io.write(encryptor.update(test_data)) io.seek(0) file_object_wrapper = EncryptedFileObjectWrapper(io, decryptor) decrypted_data = file_object_wrapper.read(len(test_data)) self.assertEqual(test_data, decrypted_data)
def _run_login_encryption(self): # Set up protocol encryption with the client, then return. server_token = b'\x89\x82\x9a\x01' # Guaranteed to be random. self.write_packet(clientbound.login.EncryptionRequestPacket( server_id='', verify_token=server_token, public_key=self.server.public_key_bytes)) packet = self.read_packet() assert isinstance(packet, serverbound.login.EncryptionResponsePacket) private_key = self.server.private_key client_token = private_key.decrypt(packet.verify_token, PKCS1v15()) assert client_token == server_token shared_secret = private_key.decrypt(packet.shared_secret, PKCS1v15()) cipher = create_AES_cipher(shared_secret) enc, dec = cipher.encryptor(), cipher.decryptor() self.socket = EncryptedSocketWrapper(self.socket, enc, dec) self.socket_file = EncryptedFileObjectWrapper(self.socket_file, dec)
class FakeClientHandler(object): """ Represents a single client connection being handled by a 'FakeServer'. The methods of the form 'handle_*' may be overridden by subclasses to customise the behaviour of the server. """ __slots__ = 'server', 'socket', 'socket_file', 'packets', \ 'compression_enabled', 'user_uuid', 'user_name' def __init__(self, server, socket): self.server = server self.socket = socket self.socket_file = socket.makefile('rb', 0) self.compression_enabled = False self.user_uuid = None self.user_name = None def run(self): # Communicate with the client until disconnected. try: self._run_handshake() self.socket.shutdown(socket.SHUT_RDWR) finally: self.socket.close() self.socket_file.close() def handle_connection(self): # Called in the handshake state, just after the client connects, # before any packets have been exchanged. pass def handle_handshake(self, handshake_packet): # Called in the handshake state, after receiving the client's # Handshake packet, which is provided as an argument. pass def handle_login(self, login_start_packet): # Called to transition from the login state to the play state, after # compression and encryption, if applicable, have been set up. The # client's LoginStartPacket is given as an argument. self.user_name = login_start_packet.name self.user_uuid = uuid.UUID(bytes=hashlib.md5( ('OfflinePlayer:%s' % self.user_name).encode('utf8')).digest()) self.write_packet(clientbound.login.LoginSuccessPacket( UUID=str(self.user_uuid), Username=self.user_name)) def handle_play_start(self): # Called upon entering the play state. packet = clientbound.play.JoinGamePacket( entity_id=0, is_hardcore=False, game_mode=0, previous_game_mode=0, world_names=['minecraft:overworld'], world_name='minecraft:overworld', hashed_seed=12345, difficulty=2, max_players=1, level_type='default', reduced_debug_info=False, render_distance=9, respawn_screen=False, is_debug=False, is_flat=False) if self.server.context.protocol_version >= 748: packet.dimension = pynbt.TAG_Compound({ 'natural': pynbt.TAG_Byte(1), 'effects': pynbt.TAG_String('minecraft:overworld'), }, '') packet.dimension_codec = pynbt.TAG_Compound({ 'minecraft:dimension_type': pynbt.TAG_Compound({ 'type': pynbt.TAG_String('minecraft:dimension_type'), 'value': pynbt.TAG_List(pynbt.TAG_Compound, [ pynbt.TAG_Compound(packet.dimension), ]), }), 'minecraft:worldgen/biome': pynbt.TAG_Compound({ 'type': pynbt.TAG_String('minecraft:worldgen/biome'), 'value': pynbt.TAG_List(pynbt.TAG_Compound, [ pynbt.TAG_Compound({ 'id': pynbt.TAG_Int(1), 'name': pynbt.TAG_String('minecraft:plains'), }), pynbt.TAG_Compound({ 'id': pynbt.TAG_Int(2), 'name': pynbt.TAG_String('minecraft:desert'), }), ]), }), }, '') elif self.server.context.protocol_version >= 718: packet.dimension = 'minecraft:overworld' else: packet.dimension = types.Dimension.OVERWORLD self.write_packet(packet) def handle_play_packet(self, packet): # Called upon each packet received after handle_play_start() returns. if isinstance(packet, serverbound.play.ChatPacket): assert len(packet.message) <= packet.max_length self.write_packet(clientbound.play.ChatMessagePacket(json.dumps({ 'translate': 'chat.type.text', 'with': [self.username, packet.message], }))) def handle_status(self, request_packet): # Called in the first phase of the status state, to send the Response # packet. The client's Request packet is provided as an argument. packet = clientbound.status.ResponsePacket() packet.json_response = json.dumps({ 'version': { 'name': self.server.minecraft_version, 'protocol': self.server.context.protocol_version}, 'players': { 'max': 1, 'online': 0, 'sample': []}, 'description': { 'text': 'FakeServer'}}) self.write_packet(packet) def handle_ping(self, ping_packet): # Called in the second phase of the status state, to respond to a Ping # packet, which is provided as an argument. packet = clientbound.status.PingResponsePacket(time=ping_packet.time) self.write_packet(packet) def handle_login_server_disconnect(self, message): # Called when the server cleanly terminates the connection during # login, i.e. by raising FakeServerDisconnect from a handler. message = 'Connection denied.' if message is None else message self.write_packet(clientbound.login.DisconnectPacket( json_data=json.dumps({'text': message}))) def handle_play_server_disconnect(self, message): # As 'handle_login_server_disconnect', but for the play state. message = 'Disconnected.' if message is None else message self.write_packet(clientbound.play.DisconnectPacket( json_data=json.dumps({'text': message}))) def handle_play_client_disconnect(self): # Called when the client cleanly terminates the connection during play. pass def write_packet(self, packet): # Send and log a clientbound packet. packet.context = self.server.context logging.debug('[S-> ] %s' % packet) packet.write(self.socket, **( {'compression_threshold': self.server.compression_threshold} if self.compression_enabled else {})) def read_packet(self): # Read and log a serverbound packet from the client, or raises # FakeClientDisconnect if the client has cleanly disconnected. buffer = self._read_packet_buffer() packet_id = types.VarInt.read(buffer) if packet_id in self.packets: packet = self.packets[packet_id](self.server.context) packet.read(buffer) else: packet = packets.Packet(self.server.context, id=packet_id) logging.debug('[ ->S] %s' % packet) return packet def _run_handshake(self): # Enter the initial (i.e. handshaking) state of the connection. self.packets = self.server.packets_handshake try: self.handle_connection() packet = self.read_packet() assert isinstance(packet, serverbound.handshake.HandShakePacket), \ type(packet) self.handle_handshake(packet) if packet.next_state == 1: self._run_status() elif packet.next_state == 2: self._run_handshake_play(packet) else: raise AssertionError('Unknown state: %s' % packet.next_state) except FakeServerDisconnect: pass def _run_handshake_play(self, packet): # Prepare to transition from handshaking to play state (via login), # using the given serverbound HandShakePacket to perform play-specific # processing. if packet.protocol_version == self.server.context.protocol_version: return self._run_login() if packet.protocol_version < self.server.context.protocol_version: msg = 'Outdated client! Please use %s' \ % self.server.minecraft_version else: msg = "Outdated server! I'm still on %s" \ % self.server.minecraft_version self.handle_login_server_disconnect(msg) def _run_login(self): # Enter the login state of the connection. self.packets = self.server.packets_login packet = self.read_packet() assert isinstance(packet, serverbound.login.LoginStartPacket) if self.server.private_key is not None: self._run_login_encryption() if self.server.compression_threshold is not None: self.write_packet(clientbound.login.SetCompressionPacket( threshold=self.server.compression_threshold)) self.compression_enabled = True try: self.handle_login(packet) except FakeServerDisconnect as e: self.handle_login_server_disconnect(message=e.message) else: self._run_playing() def _run_login_encryption(self): # Set up protocol encryption with the client, then return. server_token = b'\x89\x82\x9a\x01' # Guaranteed to be random. self.write_packet(clientbound.login.EncryptionRequestPacket( server_id='', verify_token=server_token, public_key=self.server.public_key_bytes)) packet = self.read_packet() assert isinstance(packet, serverbound.login.EncryptionResponsePacket) private_key = self.server.private_key client_token = private_key.decrypt(packet.verify_token, PKCS1v15()) assert client_token == server_token shared_secret = private_key.decrypt(packet.shared_secret, PKCS1v15()) cipher = create_AES_cipher(shared_secret) enc, dec = cipher.encryptor(), cipher.decryptor() self.socket = EncryptedSocketWrapper(self.socket, enc, dec) self.socket_file = EncryptedFileObjectWrapper(self.socket_file, dec) def _run_playing(self): # Enter the playing state of the connection. self.packets = self.server.packets_playing client_disconnected = False try: self.handle_play_start() try: while True: self.handle_play_packet(self.read_packet()) except FakeClientDisconnect: client_disconnected = True self.handle_play_client_disconnect() except FakeServerDisconnect as e: if not client_disconnected: self.handle_play_server_disconnect(message=e.message) def _run_status(self): # Enter the status state of the connection. self.packets = self.server.packets_status packet = self.read_packet() assert isinstance(packet, serverbound.status.RequestPacket) try: self.handle_status(packet) try: packet = self.read_packet() except FakeClientDisconnect: return assert isinstance(packet, serverbound.status.PingPacket) self.handle_ping(packet) except FakeServerDisconnect: pass def _read_packet_buffer(self): # Read a serverbound packet in the form of a raw buffer, or raises # FakeClientDisconnect if the client has cleanly disconnected. try: length = types.VarInt.read(self.socket_file) except EOFError: raise FakeClientDisconnect buffer = packets.PacketBuffer() while len(buffer.get_writable()) < length: data = self.socket_file.read(length - len(buffer.get_writable())) buffer.send(data) buffer.reset_cursor() if self.compression_enabled: data_length = types.VarInt.read(buffer) if data_length > 0: data = zlib.decompress(buffer.read()) assert len(data) == data_length, \ '%s != %s' % (len(data), data_length) buffer.reset() buffer.send(data) buffer.reset_cursor() return buffer
class FakeClientHandler(object): """ Represents a single client connection being handled by a 'FakeServer'. The methods of the form 'handle_*' may be overridden by subclasses to customise the behaviour of the server. """ __slots__ = 'server', 'socket', 'socket_file', 'packets', \ 'compression_enabled', 'user_uuid', 'user_name' def __init__(self, server, socket): self.server = server self.socket = socket self.socket_file = socket.makefile('rb', 0) self.compression_enabled = False self.user_uuid = None self.user_name = None def run(self): # Communicate with the client until disconnected. try: self._run_handshake() self.socket.shutdown(socket.SHUT_RDWR) finally: self.socket.close() self.socket_file.close() def handle_connection(self): # Called in the handshake state, just after the client connects, # before any packets have been exchanged. pass def handle_handshake(self, handshake_packet): # Called in the handshake state, after receiving the client's # Handshake packet, which is provided as an argument. pass def handle_login(self, login_start_packet): # Called to transition from the login state to the play state, after # compression and encryption, if applicable, have been set up. The # client's LoginStartPacket is given as an argument. self.user_name = login_start_packet.name self.user_uuid = uuid.UUID(bytes=hashlib.md5( ('OfflinePlayer:%s' % self.user_name).encode('utf8')).digest()) self.write_packet(clientbound.login.LoginSuccessPacket( UUID=str(self.user_uuid), Username=self.user_name)) def handle_play_start(self): # Called upon entering the play state. self.write_packet(clientbound.play.JoinGamePacket( entity_id=0, game_mode=0, dimension=0, difficulty=2, max_players=1, level_type='default', reduced_debug_info=False, render_distance=9)) def handle_play_packet(self, packet): # Called upon each packet received after handle_play_start() returns. if isinstance(packet, serverbound.play.ChatPacket): assert len(packet.message) <= packet.max_length self.write_packet(clientbound.play.ChatMessagePacket(json.dumps({ 'translate': 'chat.type.text', 'with': [self.username, packet.message], }))) def handle_status(self, request_packet): # Called in the first phase of the status state, to send the Response # packet. The client's Request packet is provided as an argument. packet = clientbound.status.ResponsePacket() packet.json_response = json.dumps({ 'version': { 'name': self.server.minecraft_version, 'protocol': self.server.context.protocol_version}, 'players': { 'max': 1, 'online': 0, 'sample': []}, 'description': { 'text': 'FakeServer'}}) self.write_packet(packet) def handle_ping(self, ping_packet): # Called in the second phase of the status state, to respond to a Ping # packet, which is provided as an argument. packet = clientbound.status.PingResponsePacket(time=ping_packet.time) self.write_packet(packet) def handle_login_server_disconnect(self, message): # Called when the server cleanly terminates the connection during # login, i.e. by raising FakeServerDisconnect from a handler. message = 'Connection denied.' if message is None else message self.write_packet(clientbound.login.DisconnectPacket( json_data=json.dumps({'text': message}))) def handle_play_server_disconnect(self, message): # As 'handle_login_server_disconnect', but for the play state. message = 'Disconnected.' if message is None else message self.write_packet(clientbound.play.DisconnectPacket( json_data=json.dumps({'text': message}))) def handle_play_client_disconnect(self): # Called when the client cleanly terminates the connection during play. pass def write_packet(self, packet): # Send and log a clientbound packet. packet.context = self.server.context logging.debug('[S-> ] %s' % packet) packet.write(self.socket, **( {'compression_threshold': self.server.compression_threshold} if self.compression_enabled else {})) def read_packet(self): # Read and log a serverbound packet from the client, or raises # FakeClientDisconnect if the client has cleanly disconnected. buffer = self._read_packet_buffer() packet_id = types.VarInt.read(buffer) if packet_id in self.packets: packet = self.packets[packet_id](self.server.context) packet.read(buffer) else: packet = packets.Packet(self.server.context, id=packet_id) logging.debug('[ ->S] %s' % packet) return packet def _run_handshake(self): # Enter the initial (i.e. handshaking) state of the connection. self.packets = self.server.packets_handshake try: self.handle_connection() packet = self.read_packet() assert isinstance(packet, serverbound.handshake.HandShakePacket) self.handle_handshake(packet) if packet.next_state == 1: self._run_status() elif packet.next_state == 2: self._run_handshake_play(packet) else: raise AssertionError('Unknown state: %s' % packet.next_state) except FakeServerDisconnect: pass def _run_handshake_play(self, packet): # Prepare to transition from handshaking to play state (via login), # using the given serverbound HandShakePacket to perform play-specific # processing. if packet.protocol_version == self.server.context.protocol_version: return self._run_login() if packet.protocol_version < self.server.context.protocol_version: msg = 'Outdated client! Please use %s' \ % self.server.minecraft_version else: msg = "Outdated server! I'm still on %s" \ % self.server.minecraft_version self.handle_login_server_disconnect(msg) def _run_login(self): # Enter the login state of the connection. self.packets = self.server.packets_login packet = self.read_packet() assert isinstance(packet, serverbound.login.LoginStartPacket) if self.server.private_key is not None: self._run_login_encryption() if self.server.compression_threshold is not None: self.write_packet(clientbound.login.SetCompressionPacket( threshold=self.server.compression_threshold)) self.compression_enabled = True try: self.handle_login(packet) except FakeServerDisconnect as e: self.handle_login_server_disconnect(message=e.message) else: self._run_playing() def _run_login_encryption(self): # Set up protocol encryption with the client, then return. server_token = b'\x89\x82\x9a\x01' # Guaranteed to be random. self.write_packet(clientbound.login.EncryptionRequestPacket( server_id='', verify_token=server_token, public_key=self.server.public_key_bytes)) packet = self.read_packet() assert isinstance(packet, serverbound.login.EncryptionResponsePacket) private_key = self.server.private_key client_token = private_key.decrypt(packet.verify_token, PKCS1v15()) assert client_token == server_token shared_secret = private_key.decrypt(packet.shared_secret, PKCS1v15()) cipher = create_AES_cipher(shared_secret) enc, dec = cipher.encryptor(), cipher.decryptor() self.socket = EncryptedSocketWrapper(self.socket, enc, dec) self.socket_file = EncryptedFileObjectWrapper(self.socket_file, dec) def _run_playing(self): # Enter the playing state of the connection. self.packets = self.server.packets_playing client_disconnected = False try: self.handle_play_start() try: while True: self.handle_play_packet(self.read_packet()) except FakeClientDisconnect: client_disconnected = True self.handle_play_client_disconnect() except FakeServerDisconnect as e: if not client_disconnected: self.handle_play_server_disconnect(message=e.message) def _run_status(self): # Enter the status state of the connection. self.packets = self.server.packets_status packet = self.read_packet() assert isinstance(packet, serverbound.status.RequestPacket) try: self.handle_status(packet) try: packet = self.read_packet() except FakeClientDisconnect: return assert isinstance(packet, serverbound.status.PingPacket) self.handle_ping(packet) except FakeServerDisconnect: pass def _read_packet_buffer(self): # Read a serverbound packet in the form of a raw buffer, or raises # FakeClientDisconnect if the client has cleanly disconnected. try: length = types.VarInt.read(self.socket_file) except EOFError: raise FakeClientDisconnect buffer = packets.PacketBuffer() while len(buffer.get_writable()) < length: data = self.socket_file.read(length - len(buffer.get_writable())) buffer.send(data) buffer.reset_cursor() if self.compression_enabled: data_length = types.VarInt.read(buffer) if data_length > 0: data = zlib.decompress(buffer.read()) assert len(data) == data_length, \ '%s != %s' % (len(data), data_length) buffer.reset() buffer.send(data) buffer.reset_cursor() return buffer