Example #1
0
    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)
Example #2
0
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
Example #3
0
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