Source code for quarry.net.protocol

import logging
from twisted.internet import protocol

from quarry.data import packets
from quarry.types.buffer import BufferUnderrun, buff_types
from quarry.net.crypto import Cipher
from quarry.net.ticker import Ticker

logging.basicConfig(format="%(name)s | %(levelname)s | %(message)s")

protocol_modes = {
    0: 'init',
    1: 'status',
    2: 'login',
    3: 'play'
}
protocol_modes_inv = dict(((v, k) for k, v in protocol_modes.items()))


class ProtocolError(Exception):
    pass


class PacketDispatcher(object):
    def dispatch(self, lookup_args, buff):
        handler = getattr(self, "packet_%s" % "_".join(lookup_args), None)
        if handler is not None:
            handler(buff)
            return True
        return False


[docs]class Protocol(protocol.Protocol, PacketDispatcher, object): """Shared logic between the client and server""" #: Usually a reference to a :class:`~quarry.types.buffer.Buffer` class. #: This is useful when constructing a packet payload for use in #: :meth:`send_packet` buff_type = None #: The logger for this protocol. logger = None #: A reference to a :class:`~quarry.net.ticker.Ticker` instance. ticker = None #: A reference to the factory factory = None #: The IP address of the remote. remote_addr = None recv_direction = None send_direction = None protocol_version = packets.default_protocol_version protocol_mode = "init" compression_threshold = -1 in_game = False closed = False def __init__(self, factory, remote_addr): self.factory = factory self.remote_addr = remote_addr self.buff_type = self.factory.get_buff_type(self.protocol_version) self.recv_buff = self.buff_type() self.cipher = Cipher() self.logger = logging.getLogger("%s{%s}" % ( self.__class__.__name__, self.remote_addr.host)) self.logger.setLevel(self.factory.log_level) self.ticker = self.factory.ticker_type(self.logger) self.ticker.start() self.connection_timer = self.ticker.add_delay( delay=self.factory.connection_timeout / self.ticker.interval, callback=self.connection_timed_out) self.setup() # Fix ugly twisted methods ------------------------------------------------ def dataReceived(self, data): return self.data_received(data) def connectionMade(self): return self.connection_made() def connectionLost(self, reason=None): return self.connection_lost(reason) # Convenience functions --------------------------------------------------- def check_protocol_mode_switch(self, mode): transitions = [ ("init", "status"), ("init", "login"), ("login", "play") ] if (self.protocol_mode, mode) not in transitions: raise ProtocolError("Cannot switch protocol mode from %s to %s" % (self.protocol_mode, mode)) def switch_protocol_mode(self, mode): self.check_protocol_mode_switch(mode) self.protocol_mode = mode def set_compression(self, compression_threshold): self.compression_threshold = compression_threshold self.logger.debug("Compression threshold set to %d bytes" % compression_threshold)
[docs] def close(self, reason=None): """Closes the connection""" if not self.closed: if reason: reason = "Closing connection: %s" % reason else: reason = "Closing connection" if self.in_game: self.logger.info(reason) else: self.logger.debug(reason) self.transport.loseConnection() self.closed = True
[docs] def log_packet(self, prefix, name): """Logs a packet at debug level""" self.logger.debug("Packet %s %s/%s" % ( prefix, self.protocol_mode, name))
# General callbacks ------------------------------------------------------- def setup(self): """Called when the Protocol's initialiser is finished""" pass def protocol_error(self, err): """Called when a protocol error occurs""" self.logger.exception(err) self.close("Protocol error") # Connection callbacks ----------------------------------------------------
[docs] def connection_made(self): """Called when the connection is established""" self.logger.debug("Connection made")
[docs] def connection_lost(self, reason=None): """Called when the connection is lost""" self.closed = True if self.in_game: self.player_left() self.logger.debug("Connection lost") self.ticker.stop()
[docs] def connection_timed_out(self): """Called when the connection has been idle too long""" self.close("Connection timed out")
# Auth callbacks ----------------------------------------------------------
[docs] def auth_ok(self, data): """Called when auth with mojang succeeded (online mode only)""" pass
[docs] def auth_failed(self, err): """Called when auth with mojang failed (online mode only)""" self.logger.warning("Auth failed: %s" % err.value) self.close("Auth failed: %s" % err.value)
# Player callbacks --------------------------------------------------------
[docs] def player_joined(self): """Called when the player joins the game""" self.in_game = True
[docs] def player_left(self): """Called when the player leaves the game""" pass
# Packet handling ---------------------------------------------------------
[docs] def get_packet_name(self, ident): key = (self.protocol_version, self.protocol_mode, self.recv_direction, ident) try: return packets.packet_names[key] except KeyError: raise ProtocolError("No name known for packet: %s" % (key,))
[docs] def get_packet_ident(self, name): key = (self.protocol_version, self.protocol_mode, self.send_direction, name) try: return packets.packet_idents[key] except KeyError: raise ProtocolError("No ID known for packet: %s" % (key,))
def data_received(self, data): # Decrypt data data = self.cipher.decrypt(data) # Add it to our buffer self.recv_buff.add(data) # Read some packets while not self.closed: # Save the buffer, in case we read an incomplete packet self.recv_buff.save() # Read the packet try: buff = self.recv_buff.unpack_packet( self.buff_type, self.compression_threshold) except BufferUnderrun: self.recv_buff.restore() break try: # Identify the packet name = self.get_packet_name(buff.unpack_varint()) # Dispatch the packet try: self.packet_received(buff, name) except BufferUnderrun: raise ProtocolError("Packet is too short: %s" % name) if len(buff) > 0: raise ProtocolError("Packet is too long: %s" % name) # Reset the inactivity timer self.connection_timer.restart() except ProtocolError as e: self.protocol_error(e)
[docs] def packet_received(self, buff, name): """ Called when a packet is received from the remote. Usually this method dispatches the packet to a method named ``packet_<packet name>``, or calls :meth:`packet_unhandled` if no such methods exists. You might want to override this to implement your own dispatch logic or logging. """ self.log_packet(". recv", name) dispatched = self.dispatch((name,), buff) if not dispatched: self.packet_unhandled(buff, name)
[docs] def packet_unhandled(self, buff, name): """ Called when a packet is received that is not hooked. The default implementation silently discards the packet. """ buff.discard()
[docs] def send_packet(self, name, *data): """Sends a packet to the remote.""" if self.closed: return self.log_packet("# send", name) data = b"".join(data) # Prepend ident data = self.buff_type.pack_varint(self.get_packet_ident(name)) + data # Pack packet data = self.buff_type.pack_packet(data, self.compression_threshold) # Encrypt data = self.cipher.encrypt(data) # Send self.transport.write(data)
class Factory(protocol.Factory, object): protocol = Protocol ticker_type = Ticker log_level = logging.INFO connection_timeout = 30 force_protocol_version = None minecraft_versions = packets.minecraft_versions def buildProtocol(self, addr): return self.protocol(self, addr) def get_buff_type(self, protocol_version): """ Gets a buffer type for the given protocol version. """ for ver, cls in reversed(buff_types): if protocol_version >= ver: return cls