Пример #1
0
    def configure_last_will(self, topic, payload, qos, retain=False):
        """
        Configures a message to be send as the client's last will. This message
        will be send when the connected is disconnected by a connection timeout,
        protocol error or an unexpected disconnection.

        :param str topic: The topic which the message will be delivered;
        :param bytes payload: Message payload;
        :param bool retain: Message's retain flag.

        """
        if topic is not None and payload is not None and qos is not None:
            self.last_will = Publish(qos=qos, dup=False, retain=retain)
            self.last_will.topic = topic
            self.last_will.payload = payload
Пример #2
0
    def enqueue_retained_message(self, client, subscription_mask):
        """
        Enqueues all retained messages matching the `subscription_mask` to be
        sent to the `client`.

        :param MQTTClient client: A known MQTTClient.
        :param str subscription_mask: The subscription mask to match the
          messages against.
        """

        # print(self._retained_messages._messages)
        assert isinstance(client, MQTTClient)
        for e in self._retained_messages.items():
            print("INFO: {}".format(e))
        for topic, (message, sender_uid) in self._retained_messages.items():
            # XXX Packet loop restriction #4: no forwarding to sender if sender
            # also receives subscriptions.
            if client.uid == sender_uid and client.receive_subscriptions:
                continue

            if message is not None:
                msg_obj = Publish.from_bytes(message)
                qos = client.get_matching_qos(msg_obj, subscription_mask)

                if qos is not None:
                    # creates a copy of the object to avoid reference errors
                    msg_copy = msg_obj.copy()
                    msg_copy.qos = qos
                    client.publish(msg_copy)
Пример #3
0
    def enqueue_retained_message(self, client, subscription_mask):
        """
        Enqueues all retained messages matching the `subscription_mask` to be
        sent to the `client`.

        :param MQTTClient client: A known MQTTClient.
        :param str subscription_mask: The subscription mask to match the
          messages against.
        """

        # print(self._retained_messages._messages)
        assert isinstance(client, MQTTClient)
        for e in self._retained_messages.items():
            print("INFO: {}".format(e))
        for topic, (message, sender_uid) in self._retained_messages.items():
            # XXX Packet loop restriction #4: no forwarding to sender if sender
            # also receives subscriptions.
            if client.uid == sender_uid and client.receive_subscriptions:
                continue

            if message is not None:
                msg_obj = Publish.from_bytes(message)
                qos = client.get_matching_qos(msg_obj, subscription_mask)

                if qos is not None:
                    # creates a copy of the object to avoid reference errors
                    msg_copy = msg_obj.copy()
                    msg_copy.qos = qos
                    client.publish(msg_copy)
Пример #4
0
class TestPublishMessage(TestCase):
    def setUp(self):
        self.msg = Publish()
        self.msg.id = 1
        self.msg.topic = '/foobar'

    def test_hash_for_small_msg(self):
        self.msg.payload = b'abcd'
        self.assertEqual(self.msg.raw_data, self.msg.__hash__())

    def test_hash_for_large_msg(self):
        self.msg.payload = 64 * b'abcd'

        precal_digest = b"x\xb5\xc2\xdfJ\xd0'hp\x1d\xaeV\xa1\xec\xa9\x98" \
                        b"\xc8\xcb7\xd8\xb4a_\xb9\xfc\xb4\x8a$\x92\xf6\xceY"

        self.assertEqual(precal_digest, self.msg.__hash__())
Пример #5
0
    def configure_last_will(self,  topic, payload, qos, retain=False):
        """
        Configures a message to be send as the client's last will. This message
        will be send when the connected is disconnected by a connection timeout,
        protocol error or an unexpected disconnection.

        :param str topic: The topic which the message will be delivered;
        :param bytes payload: Message payload;
        :param bool retain: Message's retain flag.

        """
        if topic is not None and payload is not None and qos is not None:
            self.last_will = Publish(qos=qos, dup=False, retain=retain)
            self.last_will.topic = topic
            self.last_will.payload = payload
Пример #6
0
class MQTTClient():
    """
    Objects of this class encapsulate and abstract all aspects of a given
    client. A MQTTClient object may refer to a live, connected, client or a
    known client which albeit disconnected had the clean_sessions flag set to
    false and, thus, is kept by the server as an end point for routed messages.

    One may call :meth:`self.is_connected` to check whether is there a connected
    client or not.

    :param MQTTServer server: The server which the client is bound to;
    :param MQTTConnection connection: The connection to be used;
    :param str uid: A string used as the client's id;
    :param bool clean_session: The clean session flag, as per MQTT Protocol;
    :param int keep_alive: The keep alive interval, in seconds.
    :param ClientPersistenceBase persistence: An object that provides persistence
    """

    broker_re = re.compile(r'^(broker|uplink)', re.IGNORECASE) # matched against 'uid'

    def __init__(self, server, connection, authorization=None,
                 uid=None, clean_session=False,
                 keep_alive=60, persistence=None, receive_subscriptions=None):

        self.uid = uid
        self.logger = getLogger('activity.clients')
        self.persistence = persistence or InMemoryClientPersistence(uid)

        self.subscriptions = ClientSubscriptions(persistence.subscriptions)

        self._connected = Event()

        self.last_will = None
        self.connection = None
        self.clean_session = None
        self.keep_alive = None
        self.receive_subscriptions = False

        self.server = server
        self.authorization = authorization or Authorization.no_restrictions()

        # Queue of the packets ready to be delivered
        self.outgoing_queue = OutgoingQueue(self.persistence.outgoing_publishes)

        self.update_configuration(clean_session, keep_alive, receive_subscriptions)
        self.update_connection(connection)

    @property
    def incoming_packet_ids(self):
        return self.persistence.incoming_packet_ids

    @property
    def redelivery_deadline(self):
        s = self.keep_alive if self.keep_alive > 0 else 60
        return timedelta(seconds=s)

    def is_broker(self):
        """boolean whether this client claims to be a broker"""
        return not not MQTTClient.broker_re.match(self.uid)

    def update_connection(self, connection):
        """
        Updates the client's connection by disconnecting the previous one and
        configuring the new one's keep alive time according to ``keep_alive``.

        :param MQTTConnection connection: The new connection to be used;
        """
        if self.is_connected():
            self.disconnect()

        if connection is not None:
            assert isinstance(connection, MQTTConnection)

            self.connection = connection
            self.connection.set_timeout(self.keep_alive)
            self.connection.set_close_callback(self._on_stream_close)
            self.connection.set_timeout_callback(self._on_connection_timeout)

            if not self.connection.closed():
                self._connected.set()

    def update_configuration(self, clean_session=False, keep_alive=60, receive_subscriptions=None):
        """
        Updates the internal attributes.

        :param bool clean_session: A flag indicating whether this session should
          be brand new or attempt to reuse the last known session for a client
          with the same :attr:`self.uid` as this.
        :param int keep_alive: Connection's keep alive setting, in seconds.
        """
        self.clean_session = clean_session
        self.keep_alive = keep_alive
        # FIXME add parameter to calls of update_configuration(...)
        if receive_subscriptions is not None:
            self.receive_subscriptions = receive_subscriptions

    def update_authorization(self, authorization):
        self.authorization = authorization

        if not self.clean_session:
            self.unsubscribe_denied_topics()

    def start(self):
        """
        Starts the client fetching, processing and dispatching routines. Should
        be called after object instantiation or a :meth:`self.update_connection`
        call.

        The following coroutines are started:

          * :meth:`self._process_incoming_messages`
          * :meth:`self._process_outgoing_messages`
        """
        self.logger.debug("[uid: %s] starting co-routines" % self.uid)
        # put connection object in `_process_incoming_messages` scope
        # so corner cases can be handled appropriately
        self._process_incoming_packets(self.connection)
        self._process_outgoing_packets(self.connection)

    @property
    def connected(self):
        """
        An :class:`toro.Event` instance that is set whenever the client is
        connected and clear on disconnection. It's safe to wait on this property
        before stream related operations.
        """
        return self._connected

    def is_connected(self):
        """
        A shorthand for :meth:connected.is_set().
        """
        return self._connected.is_set()

    def configure_last_will(self,  topic, payload, qos, retain=False):
        """
        Configures a message to be send as the client's last will. This message
        will be send when the connected is disconnected by a connection timeout,
        protocol error or an unexpected disconnection.

        :param str topic: The topic which the message will be delivered;
        :param bytes payload: Message payload;
        :param bool retain: Message's retain flag.

        """
        if topic is not None and payload is not None and qos is not None:
            self.last_will = Publish(qos=qos, dup=False, retain=retain)
            self.last_will.topic = topic
            self.last_will.payload = payload

    def handle_last_will(self):
        """
        Checks if a client has a pending last will message and dispatches it for
        server processing.
        """
        if self.last_will is not None:
            self.dispatch_to_server(self.last_will)
            self.logger.debug("[uid: %s] Dispatched last will message %s" %
                              (self.uid, self.last_will.log_info()))

    def _get_action(self, msg, factory):
        try:
            action = factory.make(msg)
            action.bind_client(self)
            return action

        except KeyError:
            self.logger.exception("[uid: %s] Wrong message type %s" %
                                  (self.uid, type(msg)))

            self.logger.debug(
                "[uid: %s] Will be disconnect due to invalid message" %
                self.uid
            )

            self.disconnect()

    @gen.coroutine
    def _process_incoming_packets(self, connection):
        """
        This coroutinte fetches the message raw data from
        :attr:`self.incoming_queue`, parses it into the corresponding message
        object (an instance of one of the
        :class:`broker.messages.BaseMQTTMessage` subclasses) and passes it to
        the :attr:`self.incoming_transaction_manager` to be processed.

        It is started by calling :meth:`self.start()` and stops upon client
        disconnection.
        """
        while connection.is_readable:
            with client_process_context(self, connection):
                msg = yield connection.read_message()

                msg_obj = MQTTMessageFactory.make(msg)
                self.logger.debug("[B << C] [uid: %s] %s" %
                                  (self.uid, msg_obj.log_info()))

                action = self._get_action(msg_obj, IncomingActionFactory)
                assert isinstance(action, IncomingAction)
                action.run()

        self.logger.debug("[uid: %s] stopping _process_incoming_messages"
                          % self.uid)

    @gen.coroutine
    def _process_outgoing_packets(self, connection):
        """
        This coroutinte fetches the message raw data from
        :attr:`self.outgoing_queue`, parses it into the corresponding message
        object (an instance of one of the
        :class:`broker.messages.BaseMQTTMessage` subclasses) and passes
        it to the :attr:`self.outgoing_transaction_manager` to be processed.

        It is started by calling :meth:`self.start()` and stops upon client
        disconnection.
        """
        self.outgoing_queue.clear()
        self.outgoing_queue.retry_pending()

        while not connection.closed():
            with client_process_context(self, connection):
                msg = yield self.outgoing_queue.get()
                assert isinstance(msg, BaseMQTTMessage)

                action = self._get_action(msg, OutgoingActionFactory)
                assert isinstance(action, OutgoingAction)

                self.logger.debug("[B >> C] [uid: %s] %s" %
                                  (self.uid, msg.log_info()))
                yield self.write(action.get_data())

                action.post_write()

        self.logger.debug("[uid: %s] stopping _process_outgoing_messages" % self.uid)

    def publish(self, msg):
        """
        Puts a publish packet on the :attr:`self.outgoing_queue` to be sent
        to the client.

        :param msg: The message to be set or a iterable of its bytes.
        :type msg: Publish
        """
        try:
            self.outgoing_queue.put_publish(msg)
            self.logger.error('[uid: %s] Send publish packet to Subscriber with topic %s' % (self.uid, msg.topic))
        except PacketIdsDepletedError:
            self.logger.error('[uid: %s] Packet IDs depleted' % self.uid)

    def send_packet(self, packet):
        """
        Puts a packet on the :attr:`self.outgoing_queue` to be sent to the
        client.
        """
        self.outgoing_queue.put(packet)

    @gen.coroutine
    def write(self, msg):
        """
        Writes a MQTT Message to the client. If the client isn't connected,
        waits for the :attr:`self.connected` event to be set.

        :param MQTT Message msg: The message to be send. It must be a instance
          of :class:`broker.messages.BaseMQTTMessage` or it's subclasses.
        """
        yield self.connected.wait()
        yield self.connection.write_message(msg)

    def dispatch_to_server(self, pub_msg):
        """
        Dispatches a Publish message to the server for further processing, ie.
        delivering it to the appropriate subscribers.

        :param Publish pub_msg: A :class:`broker.messages.Publish` instance.
        """
        assert isinstance(pub_msg, Publish)

        if self.authorization.is_publish_allowed(pub_msg.topic):
            self.server.handle_incoming_publish(pub_msg, self.uid)
        else:
            self.logger.warn("[uid: %s] is not allowed to publish on %s" %
                             (self.uid, pub_msg.topic))

    def subscribe(self, subscription_mask, qos):
        """
        Subscribes the client to a topic or wildcarded mask at the informed QoS
        level. Calling this method also signalizes the server to enqueue the
        matching retained messages.

        When called for a (`subscripition_mask`, `qos`) pair for which the
        client has already a subscription it will silently ignore the command
        and return a suback.

        :param string subscription_mask: A MQTT valid topic or wildcarded mask;
        :param int qos: A valid QoS level (0, 1 or 2).
        :rtype: int
        :return: The granted QoS level (0, 1 or 2) or 0x80 for failed
          subscriptions.
        """
        if qos not in [0, 1, 2]:
            self.logger.warn('client tried to subscribe with invalid qos %s' % qos)
            return 0x80

        new_subscription = subscription_mask not in self.subscriptions or \
            self.subscriptions.qos(subscription_mask) != qos

        if not self.authorization.is_subscription_allowed(subscription_mask):
            self.logger.warn("[uid: %s] is not allowed to subscribe on %s" %
                             (self.uid, subscription_mask))
            del self.subscriptions[subscription_mask]
            qos = 0x80

        elif new_subscription:
            ereg = MQTTUtils.convert_to_ereg(subscription_mask)
            if ereg is not None:
                self.subscriptions.add(subscription_mask, qos, re.compile(ereg))
                self.server.enqueue_retained_message(self, subscription_mask)

            else:
                qos = 0x80
        if "#" in subscription_mask:
            print("SUBSCRIBING TO: {}".format(subscription_mask))
        return qos

    def unsubscribe(self, topics):
        """
        Unsubscribes the client from each topic in ``topics``. Safely
        ignores topics which the client is not subscribed to.

        :param iterable topics: An iterable of MQTT valid topic strings.
        """
        for topic in topics:
            del self.subscriptions[topic]

    def unsubscribe_denied_topics(self):
        for topic in self.subscriptions.masks:
            if not self.authorization.is_subscription_allowed(topic):
                self.unsubscribe(topic)

    def disconnect(self):
        """
        Closes the socket and disconnects the client. If
        :attr:`self.clean_session` is set, ensures that the incoming and
        outgoing queues are cleared and calls the server client removing
        routine.

        .. hint::
           It's safe to call this function without checking whether the
           connection is open or not.
        """
        if self.is_connected():
            self.logger.debug("[uid: %s] disconnecting client" % self.uid)
            self.connection.close()
            self._connected.clear()

        self.outgoing_queue.clear()

        if self.clean_session:
            self.logger.debug("[uid: %s] cleaning session" % self.uid)
            self.server.remove_client(self)

    def get_list_of_delivery_qos(self, msg):
        """
        Matches the ``msg.topic`` against all the current subscriptions and
        returns a list containing the QoS level for each matched subscription.

        :param Publish msg: A MQTT valid message.
        :rtype: tuple
        :return: A list of QoS levels, ie [0, 0, 1, 2, 0, 2]
        """
        # TODO CACHING THESE RESULTS PER CLIENT
        qos_list = []
        for mask in self.subscriptions.masks:
            qos = self.get_matching_qos(msg, mask)

            if qos is not None:
                qos_list.append(qos)

        return qos_list

    def get_matching_qos(self, msg, subscriptions_mask):
        """
        Matches the ``msg.topic`` against a single subscription defined by the
        subscription mask and returns the QoS level on which the message should
        be delivered.

        :param Publish msg: Message to be analysed;
        :param subscriptions_mask: A subscription mask that identifies one of
          the client's subscriptions.
        :return: QoS Level or None, in case it doesn't match.
        """
        assert isinstance(msg, Publish)

        qos, pattern = self.subscriptions[subscriptions_mask]

        if pattern.match(msg.topic) is not None:
            self.logger.debug("[uid: %s] %s matched by %s, MAXQOS: %d"
                              % (self.uid, msg.topic, subscriptions_mask, qos))

            return min(qos, msg.qos)

        return None

    def _on_connection_timeout(self, connection):
        """
        Callback called when the connection times out. Ensures clearing the
        :attr:`self.connected` event and processing the :meth:`self.disconnect`
        method.
        """
        self.logger.debug("[uid: %s] Connection timeout" % self.uid)
        self.handle_last_will()
        self.disconnect()

        connection.set_close_callback(None)
        connection.set_timeout_callback(None)

    def _on_stream_close(self, connection):
        """
        Callback called when the stream closes. Ensures clearing the
        :attr:`self.connected` event and processing the :meth:`self.disconnect`
        method.
        """
        self.logger.debug("[uid: %s] Stream closed %s" %
                          (self.uid, self.connection._address))

        try:
            if self.connection.closed_due_error():
                self.handle_last_will()

            self.disconnect()

        except MQTTConnectionClosed:
            # already closed / disconnected
            pass

        connection.set_close_callback(None)
        connection.set_timeout_callback(None)
Пример #7
0
class MQTTClient():
    """
    Objects of this class encapsulate and abstract all aspects of a given
    client. A MQTTClient object may refer to a live, connected, client or a
    known client which albeit disconnected had the clean_sessions flag set to
    false and, thus, is kept by the server as an end point for routed messages.

    One may call :meth:`self.is_connected` to check whether is there a connected
    client or not.

    :param MQTTServer server: The server which the client is bound to;
    :param MQTTConnection connection: The connection to be used;
    :param str uid: A string used as the client's id;
    :param bool clean_session: The clean session flag, as per MQTT Protocol;
    :param int keep_alive: The keep alive interval, in seconds.
    :param ClientPersistenceBase persistence: An object that provides persistence
    """

    broker_re = re.compile(r'^(broker|uplink)',
                           re.IGNORECASE)  # matched against 'uid'

    def __init__(self,
                 server,
                 connection,
                 authorization=None,
                 uid=None,
                 clean_session=False,
                 keep_alive=60,
                 persistence=None,
                 receive_subscriptions=None):

        self.uid = uid
        self.logger = getLogger('activity.clients')
        self.persistence = persistence or InMemoryClientPersistence(uid)

        self.subscriptions = ClientSubscriptions(persistence.subscriptions)

        self._connected = Event()

        self.last_will = None
        self.connection = None
        self.clean_session = None
        self.keep_alive = None
        self.receive_subscriptions = False

        self.server = server
        self.authorization = authorization or Authorization.no_restrictions()

        # Queue of the packets ready to be delivered
        self.outgoing_queue = OutgoingQueue(
            self.persistence.outgoing_publishes)

        self.update_configuration(clean_session, keep_alive,
                                  receive_subscriptions)
        self.update_connection(connection)

    @property
    def incoming_packet_ids(self):
        return self.persistence.incoming_packet_ids

    @property
    def redelivery_deadline(self):
        s = self.keep_alive if self.keep_alive > 0 else 60
        return timedelta(seconds=s)

    def is_broker(self):
        """boolean whether this client claims to be a broker"""
        return not not MQTTClient.broker_re.match(self.uid)

    def update_connection(self, connection):
        """
        Updates the client's connection by disconnecting the previous one and
        configuring the new one's keep alive time according to ``keep_alive``.

        :param MQTTConnection connection: The new connection to be used;
        """
        if self.is_connected():
            self.disconnect()

        if connection is not None:
            assert isinstance(connection, MQTTConnection)

            self.connection = connection
            self.connection.set_timeout(self.keep_alive)
            self.connection.set_close_callback(self._on_stream_close)
            self.connection.set_timeout_callback(self._on_connection_timeout)

            if not self.connection.closed():
                self._connected.set()

    def update_configuration(self,
                             clean_session=False,
                             keep_alive=60,
                             receive_subscriptions=None):
        """
        Updates the internal attributes.

        :param bool clean_session: A flag indicating whether this session should
          be brand new or attempt to reuse the last known session for a client
          with the same :attr:`self.uid` as this.
        :param int keep_alive: Connection's keep alive setting, in seconds.
        """
        self.clean_session = clean_session
        self.keep_alive = keep_alive
        # FIXME add parameter to calls of update_configuration(...)
        if receive_subscriptions is not None:
            self.receive_subscriptions = receive_subscriptions

    def update_authorization(self, authorization):
        self.authorization = authorization

        if not self.clean_session:
            self.unsubscribe_denied_topics()

    def start(self):
        """
        Starts the client fetching, processing and dispatching routines. Should
        be called after object instantiation or a :meth:`self.update_connection`
        call.

        The following coroutines are started:

          * :meth:`self._process_incoming_messages`
          * :meth:`self._process_outgoing_messages`
        """
        self.logger.debug("[uid: %s] starting co-routines" % self.uid)
        # put connection object in `_process_incoming_messages` scope
        # so corner cases can be handled appropriately
        self._process_incoming_packets(self.connection)
        self._process_outgoing_packets(self.connection)

    @property
    def connected(self):
        """
        An :class:`toro.Event` instance that is set whenever the client is
        connected and clear on disconnection. It's safe to wait on this property
        before stream related operations.
        """
        return self._connected

    def is_connected(self):
        """
        A shorthand for :meth:connected.is_set().
        """
        return self._connected.is_set()

    def configure_last_will(self, topic, payload, qos, retain=False):
        """
        Configures a message to be send as the client's last will. This message
        will be send when the connected is disconnected by a connection timeout,
        protocol error or an unexpected disconnection.

        :param str topic: The topic which the message will be delivered;
        :param bytes payload: Message payload;
        :param bool retain: Message's retain flag.

        """
        if topic is not None and payload is not None and qos is not None:
            self.last_will = Publish(qos=qos, dup=False, retain=retain)
            self.last_will.topic = topic
            self.last_will.payload = payload

    def handle_last_will(self):
        """
        Checks if a client has a pending last will message and dispatches it for
        server processing.
        """
        if self.last_will is not None:
            self.dispatch_to_server(self.last_will)
            self.logger.debug("[uid: %s] Dispatched last will message %s" %
                              (self.uid, self.last_will.log_info()))

    def _get_action(self, msg, factory):
        try:
            action = factory.make(msg)
            action.bind_client(self)
            return action

        except KeyError:
            self.logger.exception("[uid: %s] Wrong message type %s" %
                                  (self.uid, type(msg)))

            self.logger.debug(
                "[uid: %s] Will be disconnect due to invalid message" %
                self.uid)

            self.disconnect()

    @gen.coroutine
    def _process_incoming_packets(self, connection):
        """
        This coroutinte fetches the message raw data from
        :attr:`self.incoming_queue`, parses it into the corresponding message
        object (an instance of one of the
        :class:`broker.messages.BaseMQTTMessage` subclasses) and passes it to
        the :attr:`self.incoming_transaction_manager` to be processed.

        It is started by calling :meth:`self.start()` and stops upon client
        disconnection.
        """
        while connection.is_readable:
            with client_process_context(self, connection):
                msg = yield connection.read_message()

                msg_obj = MQTTMessageFactory.make(msg)
                self.logger.debug("[B << C] [uid: %s] %s" %
                                  (self.uid, msg_obj.log_info()))

                action = self._get_action(msg_obj, IncomingActionFactory)
                assert isinstance(action, IncomingAction)
                action.run()

        self.logger.debug("[uid: %s] stopping _process_incoming_messages" %
                          self.uid)

    @gen.coroutine
    def _process_outgoing_packets(self, connection):
        """
        This coroutinte fetches the message raw data from
        :attr:`self.outgoing_queue`, parses it into the corresponding message
        object (an instance of one of the
        :class:`broker.messages.BaseMQTTMessage` subclasses) and passes
        it to the :attr:`self.outgoing_transaction_manager` to be processed.

        It is started by calling :meth:`self.start()` and stops upon client
        disconnection.
        """
        self.outgoing_queue.clear()
        self.outgoing_queue.retry_pending()

        while not connection.closed():
            with client_process_context(self, connection):
                msg = yield self.outgoing_queue.get()
                assert isinstance(msg, BaseMQTTMessage)

                action = self._get_action(msg, OutgoingActionFactory)
                assert isinstance(action, OutgoingAction)

                self.logger.debug("[B >> C] [uid: %s] %s" %
                                  (self.uid, msg.log_info()))
                yield self.write(action.get_data())

                action.post_write()

        self.logger.debug("[uid: %s] stopping _process_outgoing_messages" %
                          self.uid)

    def publish(self, msg):
        """
        Puts a publish packet on the :attr:`self.outgoing_queue` to be sent
        to the client.

        :param msg: The message to be set or a iterable of its bytes.
        :type msg: Publish
        """
        try:
            self.outgoing_queue.put_publish(msg)
            self.logger.error(
                '[uid: %s] Send publish packet to Subscriber with topic %s' %
                (self.uid, msg.topic))
        except PacketIdsDepletedError:
            self.logger.error('[uid: %s] Packet IDs depleted' % self.uid)

    def send_packet(self, packet):
        """
        Puts a packet on the :attr:`self.outgoing_queue` to be sent to the
        client.
        """
        self.outgoing_queue.put(packet)

    @gen.coroutine
    def write(self, msg):
        """
        Writes a MQTT Message to the client. If the client isn't connected,
        waits for the :attr:`self.connected` event to be set.

        :param MQTT Message msg: The message to be send. It must be a instance
          of :class:`broker.messages.BaseMQTTMessage` or it's subclasses.
        """
        yield self.connected.wait()
        yield self.connection.write_message(msg)

    def dispatch_to_server(self, pub_msg):
        """
        Dispatches a Publish message to the server for further processing, ie.
        delivering it to the appropriate subscribers.

        :param Publish pub_msg: A :class:`broker.messages.Publish` instance.
        """
        assert isinstance(pub_msg, Publish)

        if self.authorization.is_publish_allowed(pub_msg.topic):
            self.server.handle_incoming_publish(pub_msg, self.uid)
        else:
            self.logger.warn("[uid: %s] is not allowed to publish on %s" %
                             (self.uid, pub_msg.topic))

    def subscribe(self, subscription_mask, qos):
        """
        Subscribes the client to a topic or wildcarded mask at the informed QoS
        level. Calling this method also signalizes the server to enqueue the
        matching retained messages.

        When called for a (`subscripition_mask`, `qos`) pair for which the
        client has already a subscription it will silently ignore the command
        and return a suback.

        :param string subscription_mask: A MQTT valid topic or wildcarded mask;
        :param int qos: A valid QoS level (0, 1 or 2).
        :rtype: int
        :return: The granted QoS level (0, 1 or 2) or 0x80 for failed
          subscriptions.
        """
        if qos not in [0, 1, 2]:
            self.logger.warn('client tried to subscribe with invalid qos %s' %
                             qos)
            return 0x80

        new_subscription = subscription_mask not in self.subscriptions or \
            self.subscriptions.qos(subscription_mask) != qos

        if not self.authorization.is_subscription_allowed(subscription_mask):
            self.logger.warn("[uid: %s] is not allowed to subscribe on %s" %
                             (self.uid, subscription_mask))
            del self.subscriptions[subscription_mask]
            qos = 0x80

        elif new_subscription:
            ereg = MQTTUtils.convert_to_ereg(subscription_mask)
            if ereg is not None:
                self.subscriptions.add(subscription_mask, qos,
                                       re.compile(ereg))
                self.server.enqueue_retained_message(self, subscription_mask)

            else:
                qos = 0x80
        if "#" in subscription_mask:
            print("SUBSCRIBING TO: {}".format(subscription_mask))
        return qos

    def unsubscribe(self, topics):
        """
        Unsubscribes the client from each topic in ``topics``. Safely
        ignores topics which the client is not subscribed to.

        :param iterable topics: An iterable of MQTT valid topic strings.
        """
        for topic in topics:
            del self.subscriptions[topic]

    def unsubscribe_denied_topics(self):
        for topic in self.subscriptions.masks:
            if not self.authorization.is_subscription_allowed(topic):
                self.unsubscribe(topic)

    def disconnect(self):
        """
        Closes the socket and disconnects the client. If
        :attr:`self.clean_session` is set, ensures that the incoming and
        outgoing queues are cleared and calls the server client removing
        routine.

        .. hint::
           It's safe to call this function without checking whether the
           connection is open or not.
        """
        if self.is_connected():
            self.logger.debug("[uid: %s] disconnecting client" % self.uid)
            self.connection.close()
            self._connected.clear()

        self.outgoing_queue.clear()

        if self.clean_session:
            self.logger.debug("[uid: %s] cleaning session" % self.uid)
            self.server.remove_client(self)

    def get_list_of_delivery_qos(self, msg):
        """
        Matches the ``msg.topic`` against all the current subscriptions and
        returns a list containing the QoS level for each matched subscription.

        :param Publish msg: A MQTT valid message.
        :rtype: tuple
        :return: A list of QoS levels, ie [0, 0, 1, 2, 0, 2]
        """
        # TODO CACHING THESE RESULTS PER CLIENT
        qos_list = []
        for mask in self.subscriptions.masks:
            qos = self.get_matching_qos(msg, mask)

            if qos is not None:
                qos_list.append(qos)

        return qos_list

    def get_matching_qos(self, msg, subscriptions_mask):
        """
        Matches the ``msg.topic`` against a single subscription defined by the
        subscription mask and returns the QoS level on which the message should
        be delivered.

        :param Publish msg: Message to be analysed;
        :param subscriptions_mask: A subscription mask that identifies one of
          the client's subscriptions.
        :return: QoS Level or None, in case it doesn't match.
        """
        assert isinstance(msg, Publish)

        qos, pattern = self.subscriptions[subscriptions_mask]

        if pattern.match(msg.topic) is not None:
            self.logger.debug("[uid: %s] %s matched by %s, MAXQOS: %d" %
                              (self.uid, msg.topic, subscriptions_mask, qos))

            return min(qos, msg.qos)

        return None

    def _on_connection_timeout(self, connection):
        """
        Callback called when the connection times out. Ensures clearing the
        :attr:`self.connected` event and processing the :meth:`self.disconnect`
        method.
        """
        self.logger.debug("[uid: %s] Connection timeout" % self.uid)
        self.handle_last_will()
        self.disconnect()

        connection.set_close_callback(None)
        connection.set_timeout_callback(None)

    def _on_stream_close(self, connection):
        """
        Callback called when the stream closes. Ensures clearing the
        :attr:`self.connected` event and processing the :meth:`self.disconnect`
        method.
        """
        self.logger.debug("[uid: %s] Stream closed %s" %
                          (self.uid, self.connection._address))

        try:
            if self.connection.closed_due_error():
                self.handle_last_will()

            self.disconnect()

        except MQTTConnectionClosed:
            # already closed / disconnected
            pass

        connection.set_close_callback(None)
        connection.set_timeout_callback(None)
Пример #8
0
 def setUp(self):
     self.msg = Publish()
     self.msg.id = 1
     self.msg.topic = '/foobar'