hbmqtt-0.9/0000755000076500000240000000000013114340722012731 5ustar nicostaff00000000000000hbmqtt-0.9/hbmqtt/0000755000076500000240000000000013114340722014230 5ustar nicostaff00000000000000hbmqtt-0.9/hbmqtt/__init__.py0000644000076500000240000000017313114340412016336 0ustar nicostaff00000000000000# Copyright (c) 2015 Nicolas JOUANIN # # See the file license.txt for copying permission. VERSION = (0, 9, 0, 'final', 0) hbmqtt-0.9/hbmqtt/adapters.py0000644000076500000240000001334513114237777016432 0ustar nicostaff00000000000000# Copyright (c) 2015 Nicolas JOUANIN # # See the file license.txt for copying permission. import asyncio import io from websockets.protocol import WebSocketCommonProtocol from websockets.exceptions import ConnectionClosed from asyncio import StreamReader, StreamWriter import logging class ReaderAdapter: """ Base class for all network protocol reader adapter. Reader adapters are used to adapt read operations on the network depending on the protocol used """ @asyncio.coroutine def read(self, n=-1) -> bytes: """ Read up to n bytes. If n is not provided, or set to -1, read until EOF and return all read bytes. If the EOF was received and the internal buffer is empty, return an empty bytes object. :return: packet read as bytes data """ def feed_eof(self): """ Acknowleddge EOF """ class WriterAdapter: """ Base class for all network protocol writer adapter. Writer adapters are used to adapt write operations on the network depending on the protocol used """ def write(self, data): """ write some data to the protocol layer """ @asyncio.coroutine def drain(self): """ Let the write buffer of the underlying transport a chance to be flushed. """ def get_peer_info(self): """ Return peer socket info (remote address and remote port as tuple """ @asyncio.coroutine def close(self): """ Close the protocol connection """ class WebSocketsReader(ReaderAdapter): """ WebSockets API reader adapter This adapter relies on WebSocketCommonProtocol to read from a WebSocket. """ def __init__(self, protocol: WebSocketCommonProtocol): self._protocol = protocol self._stream = io.BytesIO(b'') @asyncio.coroutine def read(self, n=-1) -> bytes: yield from self._feed_buffer(n) data = self._stream.read(n) return data @asyncio.coroutine def _feed_buffer(self, n=1): """ Feed the data buffer by reading a Websocket message. :param n: if given, feed buffer until it contains at least n bytes """ buffer = bytearray(self._stream.read()) while len(buffer) < n: try: message = yield from self._protocol.recv() except ConnectionClosed: message = None if message is None: break if not isinstance(message, bytes): raise TypeError("message must be bytes") buffer.extend(message) self._stream = io.BytesIO(buffer) class WebSocketsWriter(WriterAdapter): """ WebSockets API writer adapter This adapter relies on WebSocketCommonProtocol to read from a WebSocket. """ def __init__(self, protocol: WebSocketCommonProtocol): self._protocol = protocol self._stream = io.BytesIO(b'') def write(self, data): """ write some data to the protocol layer """ self._stream.write(data) @asyncio.coroutine def drain(self): """ Let the write buffer of the underlying transport a chance to be flushed. """ data = self._stream.getvalue() if len(data): yield from self._protocol.send(data) self._stream = io.BytesIO(b'') def get_peer_info(self): extra_info = self._protocol.writer.get_extra_info('peername') return extra_info[0], extra_info[1] @asyncio.coroutine def close(self): yield from self._protocol.close() class StreamReaderAdapter(ReaderAdapter): """ Asyncio Streams API protocol adapter This adapter relies on StreamReader to read from a TCP socket. Because API is very close, this class is trivial """ def __init__(self, reader: StreamReader): self._reader = reader @asyncio.coroutine def read(self, n=-1) -> bytes: return (yield from self._reader.read(n)) def feed_eof(self): return self._reader.feed_eof() class StreamWriterAdapter(WriterAdapter): """ Asyncio Streams API protocol adapter This adapter relies on StreamWriter to write to a TCP socket. Because API is very close, this class is trivial """ def __init__(self, writer: StreamWriter): self.logger = logging.getLogger(__name__) self._writer = writer def write(self, data): self._writer.write(data) @asyncio.coroutine def drain(self): yield from self._writer.drain() def get_peer_info(self): extra_info = self._writer.get_extra_info('peername') return extra_info[0], extra_info[1] @asyncio.coroutine def close(self): yield from self._writer.drain() if self._writer.can_write_eof(): self._writer.write_eof() self._writer.close() class BufferReader(ReaderAdapter): """ Byte Buffer reader adapter This adapter simply adapt reading a byte buffer. """ def __init__(self, buffer: bytes): self._stream = io.BytesIO(buffer) @asyncio.coroutine def read(self, n=-1) -> bytes: return self._stream.read(n) class BufferWriter(WriterAdapter): """ ByteBuffer writer adapter This adapter simply adapt writing to a byte buffer """ def __init__(self, buffer=b''): self._stream = io.BytesIO(buffer) def write(self, data): """ write some data to the protocol layer """ self._stream.write(data) @asyncio.coroutine def drain(self): pass def get_buffer(self): return self._stream.getvalue() def get_peer_info(self): return "BufferWriter", 0 @asyncio.coroutine def close(self): self._stream.close() hbmqtt-0.9/hbmqtt/broker.py0000644000076500000240000010453513114237777016115 0ustar nicostaff00000000000000# Copyright (c) 2015 Nicolas JOUANIN # # See the file license.txt for copying permission. import logging import ssl import websockets import asyncio import sys import re from asyncio import Queue, CancelledError if sys.version_info < (3, 5): from asyncio import async as ensure_future else: from asyncio import ensure_future from collections import deque from functools import partial from transitions import Machine, MachineError from hbmqtt.session import Session from hbmqtt.mqtt.protocol.broker_handler import BrokerProtocolHandler from hbmqtt.errors import HBMQTTException, MQTTException from hbmqtt.utils import format_client_message, gen_client_id from hbmqtt.adapters import ( StreamReaderAdapter, StreamWriterAdapter, ReaderAdapter, WriterAdapter, WebSocketsReader, WebSocketsWriter) from .plugins.manager import PluginManager, BaseContext _defaults = { 'timeout-disconnect-delay': 2, 'auth': { 'allow-anonymous': True, 'password-file': None }, } EVENT_BROKER_PRE_START = 'broker_pre_start' EVENT_BROKER_POST_START = 'broker_post_start' EVENT_BROKER_PRE_SHUTDOWN = 'broker_pre_shutdown' EVENT_BROKER_POST_SHUTDOWN = 'broker_post_shutdown' EVENT_BROKER_CLIENT_CONNECTED = 'broker_client_connected' EVENT_BROKER_CLIENT_DISCONNECTED = 'broker_client_disconnected' EVENT_BROKER_CLIENT_SUBSCRIBED = 'broker_client_subscribed' EVENT_BROKER_CLIENT_UNSUBSCRIBED = 'broker_client_unsubscribed' EVENT_BROKER_MESSAGE_RECEIVED = 'broker_message_received' class BrokerException(BaseException): pass class RetainedApplicationMessage: def __init__(self, source_session, topic, data, qos=None): self.source_session = source_session self.topic = topic self.data = data self.qos = qos class Server: def __init__(self, listener_name, server_instance, max_connections=-1, loop=None): self.logger = logging.getLogger(__name__) self.instance = server_instance self.conn_count = 0 self.listener_name = listener_name if loop is not None: self._loop = loop else: self._loop = asyncio.get_event_loop() self.max_connections = max_connections if self.max_connections > 0: self.semaphore = asyncio.Semaphore(self.max_connections, loop=self._loop) else: self.semaphore = None @asyncio.coroutine def acquire_connection(self): if self.semaphore: yield from self.semaphore.acquire() self.conn_count += 1 if self.max_connections > 0: self.logger.info("Listener '%s': %d/%d connections acquired" % (self.listener_name, self.conn_count, self.max_connections)) else: self.logger.info("Listener '%s': %d connections acquired" % (self.listener_name, self.conn_count)) def release_connection(self): if self.semaphore: self.semaphore.release() self.conn_count -= 1 if self.max_connections > 0: self.logger.info("Listener '%s': %d/%d connections acquired" % (self.listener_name, self.conn_count, self.max_connections)) else: self.logger.info("Listener '%s': %d connections acquired" % (self.listener_name, self.conn_count)) @asyncio.coroutine def close_instance(self): if self.instance: self.instance.close() yield from self.instance.wait_closed() class BrokerContext(BaseContext): """ BrokerContext is used as the context passed to plugins interacting with the broker. It act as an adapter to broker services from plugins developed for HBMQTT broker """ def __init__(self, broker): super().__init__() self.config = None self._broker_instance = broker @asyncio.coroutine def broadcast_message(self, topic, data, qos=None): yield from self._broker_instance.internal_message_broadcast(topic, data, qos) def retain_message(self, topic_name, data, qos=None): self._broker_instance.retain_message(None, topic_name, data, qos) @property def sessions(self): for k, session in self._broker_instance._sessions.items(): yield session[0] @property def retained_messages(self): return self._broker_instance._retained_messages @property def subscriptions(self): return self._broker_instance._subscriptions class Broker: """ MQTT 3.1.1 compliant broker implementation :param config: Example Yaml config :param loop: asyncio loop to use. Defaults to ``asyncio.get_event_loop()`` if none is given :param plugin_namespace: Plugin namespace to use when loading plugin entry_points. Defaults to ``hbmqtt.broker.plugins`` """ states = ['new', 'starting', 'started', 'not_started', 'stopping', 'stopped', 'not_stopped', 'stopped'] def __init__(self, config=None, loop=None, plugin_namespace=None): self.logger = logging.getLogger(__name__) self.config = _defaults if config is not None: self.config.update(config) self._build_listeners_config(self.config) if loop is not None: self._loop = loop else: self._loop = asyncio.get_event_loop() self._servers = dict() self._init_states() self._sessions = dict() self._subscriptions = dict() self._retained_messages = dict() self._broadcast_queue = asyncio.Queue(loop=self._loop) self._broadcast_task = None # Init plugins manager context = BrokerContext(self) context.config = self.config if plugin_namespace: namespace = plugin_namespace else: namespace = 'hbmqtt.broker.plugins' self.plugins_manager = PluginManager(namespace, context, self._loop) def _build_listeners_config(self, broker_config): self.listeners_config = dict() try: listeners_config = broker_config['listeners'] defaults = listeners_config['default'] for listener in listeners_config: config = dict(defaults) config.update(listeners_config[listener]) self.listeners_config[listener] = config except KeyError as ke: raise BrokerException("Listener config not found invalid: %s" % ke) def _init_states(self): self.transitions = Machine(states=Broker.states, initial='new') self.transitions.add_transition(trigger='start', source='new', dest='starting') self.transitions.add_transition(trigger='starting_fail', source='starting', dest='not_started') self.transitions.add_transition(trigger='starting_success', source='starting', dest='started') self.transitions.add_transition(trigger='shutdown', source='started', dest='stopping') self.transitions.add_transition(trigger='stopping_success', source='stopping', dest='stopped') self.transitions.add_transition(trigger='stopping_failure', source='stopping', dest='not_stopped') self.transitions.add_transition(trigger='start', source='stopped', dest='starting') @asyncio.coroutine def start(self): """ Start the broker to serve with the given configuration Start method opens network sockets and will start listening for incoming connections. This method is a *coroutine*. """ try: self._sessions = dict() self._subscriptions = dict() self._retained_messages = dict() self.transitions.start() self.logger.debug("Broker starting") except MachineError as me: self.logger.warn("[WARN-0001] Invalid method call at this moment: %s" % me) raise BrokerException("Broker instance can't be started: %s" % me) yield from self.plugins_manager.fire_event(EVENT_BROKER_PRE_START) try: # Start network listeners for listener_name in self.listeners_config: listener = self.listeners_config[listener_name] if 'bind' not in listener: self.logger.debug("Listener configuration '%s' is not bound" % listener_name) else: # Max connections try: max_connections = listener['max_connections'] except KeyError: max_connections = -1 # SSL Context sc = None # accept string "on" / "off" or boolean ssl_active = listener.get('ssl', False) if isinstance(ssl_active, str): ssl_active = ssl_active.upper() == 'ON' if ssl_active: try: sc = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH) sc.load_cert_chain(listener['certfile'], listener['keyfile']) sc.verify_mode = ssl.CERT_OPTIONAL except KeyError as ke: raise BrokerException("'certfile' or 'keyfile' configuration parameter missing: %s" % ke) except FileNotFoundError as fnfe: raise BrokerException("Can't read cert files '%s' or '%s' : %s" % (listener['certfile'], listener['keyfile'], fnfe)) address, s_port = listener['bind'].split(':') port = 0 try: port = int(s_port) except ValueError as ve: raise BrokerException("Invalid port value in bind value: %s" % listener['bind']) if listener['type'] == 'tcp': cb_partial = partial(self.stream_connected, listener_name=listener_name) instance = yield from asyncio.start_server(cb_partial, address, port, ssl=sc, loop=self._loop) self._servers[listener_name] = Server(listener_name, instance, max_connections, self._loop) elif listener['type'] == 'ws': cb_partial = partial(self.ws_connected, listener_name=listener_name) instance = yield from websockets.serve(cb_partial, address, port, ssl=sc, loop=self._loop, subprotocols=['mqtt']) self._servers[listener_name] = Server(listener_name, instance, max_connections, self._loop) self.logger.info("Listener '%s' bind to %s (max_connections=%d)" % (listener_name, listener['bind'], max_connections)) self.transitions.starting_success() yield from self.plugins_manager.fire_event(EVENT_BROKER_POST_START) #Start broadcast loop self._broadcast_task = ensure_future(self._broadcast_loop(), loop=self._loop) self.logger.debug("Broker started") except Exception as e: self.logger.error("Broker startup failed: %s" % e) self.transitions.starting_fail() raise BrokerException("Broker instance can't be started: %s" % e) @asyncio.coroutine def shutdown(self): """ Stop broker instance. Closes all connected session, stop listening on network socket and free resources. """ try: self._sessions = dict() self._subscriptions = dict() self._retained_messages = dict() self.transitions.shutdown() except MachineError as me: self.logger.debug("Invalid method call at this moment: %s" % me) raise BrokerException("Broker instance can't be stopped: %s" % me) # Fire broker_shutdown event to plugins yield from self.plugins_manager.fire_event(EVENT_BROKER_PRE_SHUTDOWN) # Stop broadcast loop if self._broadcast_task: self._broadcast_task.cancel() if self._broadcast_queue.qsize() > 0: self.logger.warning("%d messages not broadcasted" % self._broadcast_queue.qsize()) for listener_name in self._servers: server = self._servers[listener_name] yield from server.close_instance() self.logger.debug("Broker closing") self.logger.info("Broker closed") yield from self.plugins_manager.fire_event(EVENT_BROKER_POST_SHUTDOWN) self.transitions.stopping_success() @asyncio.coroutine def internal_message_broadcast(self, topic, data, qos=None): return (yield from self._broadcast_message(None, topic, data)) @asyncio.coroutine def ws_connected(self, websocket, uri, listener_name): yield from self.client_connected(listener_name, WebSocketsReader(websocket), WebSocketsWriter(websocket)) @asyncio.coroutine def stream_connected(self, reader, writer, listener_name): yield from self.client_connected(listener_name, StreamReaderAdapter(reader), StreamWriterAdapter(writer)) @asyncio.coroutine def client_connected(self, listener_name, reader: ReaderAdapter, writer: WriterAdapter): # Wait for connection available on listener server = self._servers.get(listener_name, None) if not server: raise BrokerException("Invalid listener name '%s'" % listener_name) yield from server.acquire_connection() remote_address, remote_port = writer.get_peer_info() self.logger.info("Connection from %s:%d on listener '%s'" % (remote_address, remote_port, listener_name)) # Wait for first packet and expect a CONNECT try: handler, client_session = yield from BrokerProtocolHandler.init_from_connect(reader, writer, self.plugins_manager, loop=self._loop) except HBMQTTException as exc: self.logger.warn("[MQTT-3.1.0-1] %s: Can't read first packet an CONNECT: %s" % (format_client_message(address=remote_address, port=remote_port), exc)) #yield from writer.close() self.logger.debug("Connection closed") return except MQTTException as me: self.logger.error('Invalid connection from %s : %s' % (format_client_message(address=remote_address, port=remote_port), me)) yield from writer.close() self.logger.debug("Connection closed") return if client_session.clean_session: # Delete existing session and create a new one if client_session.client_id is not None: self.delete_session(client_session.client_id) else: client_session.client_id = gen_client_id() client_session.parent = 0 else: # Get session from cache if client_session.client_id in self._sessions: self.logger.debug("Found old session %s" % repr(self._sessions[client_session.client_id])) (client_session,h) = self._sessions[client_session.client_id] client_session.parent = 1 else: client_session.parent = 0 if client_session.keep_alive > 0: client_session.keep_alive += self.config['timeout-disconnect-delay'] self.logger.debug("Keep-alive timeout=%d" % client_session.keep_alive) handler.attach(client_session, reader, writer) self._sessions[client_session.client_id] = (client_session, handler) authenticated = yield from self.authenticate(client_session, self.listeners_config[listener_name]) if not authenticated: yield from writer.close() return while True: try: client_session.transitions.connect() break except MachineError: self.logger.warning("Client %s is reconnecting too quickly, make it wait" % client_session.client_id) # Wait a bit may be client is reconnecting too fast yield from asyncio.sleep(1, loop=self._loop) yield from handler.mqtt_connack_authorize(authenticated) yield from self.plugins_manager.fire_event(EVENT_BROKER_CLIENT_CONNECTED, client_id=client_session.client_id) self.logger.debug("%s Start messages handling" % client_session.client_id) yield from handler.start() self.logger.debug("Retained messages queue size: %d" % client_session.retained_messages.qsize()) yield from self.publish_session_retained_messages(client_session) # Init and start loop for handling client messages (publish, subscribe/unsubscribe, disconnect) disconnect_waiter = ensure_future(handler.wait_disconnect(), loop=self._loop) subscribe_waiter = ensure_future(handler.get_next_pending_subscription(), loop=self._loop) unsubscribe_waiter = ensure_future(handler.get_next_pending_unsubscription(), loop=self._loop) wait_deliver = ensure_future(handler.mqtt_deliver_next_message(), loop=self._loop) connected = True while connected: try: done, pending = yield from asyncio.wait( [disconnect_waiter, subscribe_waiter, unsubscribe_waiter, wait_deliver], return_when=asyncio.FIRST_COMPLETED, loop=self._loop) if disconnect_waiter in done: result = disconnect_waiter.result() self.logger.debug("%s Result from wait_diconnect: %s" % (client_session.client_id, result)) if result is None: self.logger.debug("Will flag: %s" % client_session.will_flag) # Connection closed anormally, send will message if client_session.will_flag: self.logger.debug("Client %s disconnected abnormally, sending will message" % format_client_message(client_session)) yield from self._broadcast_message( client_session, client_session.will_topic, client_session.will_message, client_session.will_qos) if client_session.will_retain: self.retain_message(client_session, client_session.will_topic, client_session.will_message, client_session.will_qos) self.logger.debug("%s Disconnecting session" % client_session.client_id) yield from self._stop_handler(handler) client_session.transitions.disconnect() yield from self.plugins_manager.fire_event(EVENT_BROKER_CLIENT_DISCONNECTED, client_id=client_session.client_id) connected = False if unsubscribe_waiter in done: self.logger.debug("%s handling unsubscription" % client_session.client_id) unsubscription = unsubscribe_waiter.result() for topic in unsubscription['topics']: self._del_subscription(topic, client_session) yield from self.plugins_manager.fire_event( EVENT_BROKER_CLIENT_UNSUBSCRIBED, client_id=client_session.client_id, topic=topic) yield from handler.mqtt_acknowledge_unsubscription(unsubscription['packet_id']) unsubscribe_waiter = asyncio.Task(handler.get_next_pending_unsubscription(), loop=self._loop) if subscribe_waiter in done: self.logger.debug("%s handling subscription" % client_session.client_id) subscriptions = subscribe_waiter.result() return_codes = [] for subscription in subscriptions['topics']: return_codes.append(self.add_subscription(subscription, client_session)) yield from handler.mqtt_acknowledge_subscription(subscriptions['packet_id'], return_codes) for index, subscription in enumerate(subscriptions['topics']): if return_codes[index] != 0x80: yield from self.plugins_manager.fire_event( EVENT_BROKER_CLIENT_SUBSCRIBED, client_id=client_session.client_id, topic=subscription[0], qos=subscription[1]) yield from self.publish_retained_messages_for_subscription(subscription, client_session) subscribe_waiter = asyncio.Task(handler.get_next_pending_subscription(), loop=self._loop) self.logger.debug(repr(self._subscriptions)) if wait_deliver in done: if self.logger.isEnabledFor(logging.DEBUG): self.logger.debug("%s handling message delivery" % client_session.client_id) app_message = wait_deliver.result() if not app_message.topic: self.logger.warn("[MQTT-4.7.3-1] - %s invalid TOPIC sent in PUBLISH message, closing connection" % client_session.client_id) break if "#" in app_message.topic or "+" in app_message.topic: self.logger.warn("[MQTT-3.3.2-2] - %s invalid TOPIC sent in PUBLISH message, closing connection" % client_session.client_id) break yield from self.plugins_manager.fire_event(EVENT_BROKER_MESSAGE_RECEIVED, client_id=client_session.client_id, message=app_message) yield from self._broadcast_message(client_session, app_message.topic, app_message.data) if app_message.publish_packet.retain_flag: self.retain_message(client_session, app_message.topic, app_message.data, app_message.qos) wait_deliver = asyncio.Task(handler.mqtt_deliver_next_message(), loop=self._loop) except asyncio.CancelledError: self.logger.debug("Client loop cancelled") break disconnect_waiter.cancel() subscribe_waiter.cancel() unsubscribe_waiter.cancel() wait_deliver.cancel() self.logger.debug("%s Client disconnected" % client_session.client_id) server.release_connection() def _init_handler(self, session, reader, writer): """ Create a BrokerProtocolHandler and attach to a session :return: """ handler = BrokerProtocolHandler(self.plugins_manager, self._loop) handler.attach(session, reader, writer) return handler @asyncio.coroutine def _stop_handler(self, handler): """ Stop a running handler and detach if from the session :param handler: :return: """ try: yield from handler.stop() except Exception as e: self.logger.error(e) @asyncio.coroutine def authenticate(self, session: Session, listener): """ This method call the authenticate method on registered plugins to test user authentication. User is considered authenticated if all plugins called returns True. Plugins authenticate() method are supposed to return : - True if user is authentication succeed - False if user authentication fails - None if authentication can't be achieved (then plugin result is then ignored) :param session: :param listener: :return: """ auth_plugins = None auth_config = self.config.get('auth', None) if auth_config: auth_plugins = auth_config.get('plugins', None) returns = yield from self.plugins_manager.map_plugin_coro( "authenticate", session=session, filter_plugins=auth_plugins) auth_result = True if returns: for plugin in returns: res = returns[plugin] if res is False: auth_result = False self.logger.debug("Authentication failed due to '%s' plugin result: %s" % (plugin.name, res)) else: self.logger.debug("'%s' plugin result: %s" % (plugin.name, res)) # If all plugins returned True, authentication is success return auth_result def retain_message(self, source_session, topic_name, data, qos=None): if data is not None and data != b'': # If retained flag set, store the message for further subscriptions self.logger.debug("Retaining message on topic %s" % topic_name) retained_message = RetainedApplicationMessage(source_session, topic_name, data, qos) self._retained_messages[topic_name] = retained_message else: # [MQTT-3.3.1-10] if topic_name in self._retained_messages: self.logger.debug("Clear retained messages for topic '%s'" % topic_name) del self._retained_messages[topic_name] def add_subscription(self, subscription, session): import re wildcard_pattern = re.compile('.*?/?\+/?.*?') try: a_filter = subscription[0] if '#' in a_filter and not a_filter.endswith('#'): # [MQTT-4.7.1-2] Wildcard character '#' is only allowed as last character in filter return 0x80 if a_filter != "+": if '+' in a_filter: if "/+" not in a_filter and "+/" not in a_filter: # [MQTT-4.7.1-3] + wildcard character must occupy entire level return 0x80 qos = subscription[1] if 'max-qos' in self.config and qos > self.config['max-qos']: qos = self.config['max-qos'] if a_filter not in self._subscriptions: self._subscriptions[a_filter] = [] already_subscribed = next( (s for (s,qos) in self._subscriptions[a_filter] if s.client_id == session.client_id), None) if not already_subscribed: self._subscriptions[a_filter].append((session, qos)) else: self.logger.debug("Client %s has already subscribed to %s" % (format_client_message(session=session), a_filter)) return qos except KeyError: return 0x80 def _del_subscription(self, a_filter, session): """ Delete a session subscription on a given topic :param a_filter: :param session: :return: """ deleted = 0 try: subscriptions = self._subscriptions[a_filter] for index, (sub_session, qos) in enumerate(subscriptions): if sub_session.client_id == session.client_id: self.logger.debug("Removing subscription on topic '%s' for client %s" % (a_filter, format_client_message(session=session))) subscriptions.pop(index) deleted += 1 break except KeyError: # Unsubscribe topic not found in current subscribed topics pass finally: return deleted def _del_all_subscriptions(self, session): """ Delete all topic subscriptions for a given session :param session: :return: """ filter_queue = deque() for topic in self._subscriptions: if self._del_subscription(topic, session): filter_queue.append(topic) for topic in filter_queue: if not self._subscriptions[topic]: del self._subscriptions[topic] def matches(self, topic, a_filter): if "#" not in a_filter and "+" not in a_filter: # if filter doesn't contain wildcard, return exact match return a_filter == topic else: # else use regex match_pattern = re.compile(a_filter.replace('#', '.*').replace('$', '\$').replace('+', '[/\$\s\w\d]+')) return match_pattern.match(topic) @asyncio.coroutine def _broadcast_loop(self): running_tasks = deque() try: while True: while running_tasks and running_tasks[0].done(): running_tasks.popleft() broadcast = yield from self._broadcast_queue.get() if self.logger.isEnabledFor(logging.DEBUG): self.logger.debug("broadcasting %r" % broadcast) for k_filter in self._subscriptions: if broadcast['topic'].startswith("$") and (k_filter.startswith("+") or k_filter.startswith("#")): self.logger.debug("[MQTT-4.7.2-1] - ignoring brodcasting $ topic to subscriptions starting with + or #") elif self.matches(broadcast['topic'], k_filter): subscriptions = self._subscriptions[k_filter] for (target_session, qos) in subscriptions: if 'qos' in broadcast: qos = broadcast['qos'] if target_session.transitions.state == 'connected': self.logger.debug("broadcasting application message from %s on topic '%s' to %s" % (format_client_message(session=broadcast['session']), broadcast['topic'], format_client_message(session=target_session))) handler = self._get_handler(target_session) task = ensure_future( handler.mqtt_publish(broadcast['topic'], broadcast['data'], qos, retain=False), loop=self._loop) running_tasks.append(task) else: self.logger.debug("retaining application message from %s on topic '%s' to client '%s'" % (format_client_message(session=broadcast['session']), broadcast['topic'], format_client_message(session=target_session))) retained_message = RetainedApplicationMessage( broadcast['session'], broadcast['topic'], broadcast['data'], qos) yield from target_session.retained_messages.put(retained_message) except CancelledError: # Wait until current broadcasting tasks end if running_tasks: yield from asyncio.wait(running_tasks, loop=self._loop) @asyncio.coroutine def _broadcast_message(self, session, topic, data, force_qos=None): broadcast = { 'session': session, 'topic': topic, 'data': data } if force_qos: broadcast['qos'] = force_qos yield from self._broadcast_queue.put(broadcast) @asyncio.coroutine def publish_session_retained_messages(self, session): self.logger.debug("Publishing %d messages retained for session %s" % (session.retained_messages.qsize(), format_client_message(session=session)) ) publish_tasks = [] handler = self._get_handler(session) while not session.retained_messages.empty(): retained = yield from session.retained_messages.get() publish_tasks.append(ensure_future( handler.mqtt_publish( retained.topic, retained.data, retained.qos, True), loop=self._loop)) if publish_tasks: yield from asyncio.wait(publish_tasks, loop=self._loop) @asyncio.coroutine def publish_retained_messages_for_subscription(self, subscription, session): self.logger.debug("Begin broadcasting messages retained due to subscription on '%s' from %s" % (subscription[0], format_client_message(session=session))) publish_tasks = [] handler = self._get_handler(session) for d_topic in self._retained_messages: self.logger.debug("matching : %s %s" % (d_topic, subscription[0])) if self.matches(d_topic, subscription[0]): self.logger.debug("%s and %s match" % (d_topic, subscription[0])) retained = self._retained_messages[d_topic] publish_tasks.append(asyncio.Task( handler.mqtt_publish( retained.topic, retained.data, subscription[1], True), loop=self._loop)) if publish_tasks: yield from asyncio.wait(publish_tasks, loop=self._loop) self.logger.debug("End broadcasting messages retained due to subscription on '%s' from %s" % (subscription[0], format_client_message(session=session))) def delete_session(self, client_id): """ Delete an existing session data, for example due to clean session set in CONNECT :param client_id: :return: """ try: session = self._sessions[client_id][0] except KeyError: session = None if session is None: self.logger.debug("Delete session : session %s doesn't exist" % client_id) return # Delete subscriptions self.logger.debug("deleting session %s subscriptions" % repr(session)) self._del_all_subscriptions(session) self.logger.debug("deleting existing session %s" % repr(self._sessions[client_id])) del self._sessions[client_id] def _get_handler(self, session): client_id = session.client_id if client_id: try: return self._sessions[client_id][1] except KeyError: pass return None hbmqtt-0.9/hbmqtt/client.py0000644000076500000240000005213613114337300016065 0ustar nicostaff00000000000000# Copyright (c) 2015 Nicolas JOUANIN # # See the file license.txt for copying permission. import logging import ssl from urllib.parse import urlparse, urlunparse from functools import wraps from hbmqtt.utils import not_in_dict_or_none from hbmqtt.session import Session from hbmqtt.mqtt.connack import * from hbmqtt.mqtt.connect import * from hbmqtt.mqtt.protocol.client_handler import ClientProtocolHandler from hbmqtt.adapters import StreamReaderAdapter, StreamWriterAdapter, WebSocketsReader, WebSocketsWriter from hbmqtt.plugins.manager import PluginManager, BaseContext from hbmqtt.mqtt.protocol.handler import EVENT_MQTT_PACKET_SENT, EVENT_MQTT_PACKET_RECEIVED, ProtocolHandlerException from hbmqtt.mqtt.constants import * import websockets from websockets.uri import InvalidURI from websockets.handshake import InvalidHandshake from collections import deque import sys if sys.version_info < (3, 5): from asyncio import async as ensure_future else: from asyncio import ensure_future _defaults = { 'keep_alive': 10, 'ping_delay': 1, 'default_qos': 0, 'default_retain': False, 'auto_reconnect': True, 'reconnect_max_interval': 10, 'reconnect_retries': 2, } class ClientException(Exception): pass class ConnectException(ClientException): pass class ClientContext(BaseContext): """ ClientContext is used as the context passed to plugins interacting with the client. It act as an adapter to client services from plugins """ def __init__(self): super().__init__() self.config = None base_logger = logging.getLogger(__name__) def mqtt_connected(func): """ MQTTClient coroutines decorator which will wait until connection before calling the decorated method. :param func: coroutine to be called once connected :return: coroutine result """ @asyncio.coroutine @wraps(func) def wrapper(self, *args, **kwargs): if not self._connected_state.is_set(): base_logger.warning("Client not connected, waiting for it") yield from self._connected_state.wait() return (yield from func(self, *args, **kwargs)) return wrapper class MQTTClient: """ MQTT client implementation. MQTTClient instances provides API for connecting to a broker and send/receive messages using the MQTT protocol. :param client_id: MQTT client ID to use when connecting to the broker. If none, it will generated randomly by :func:`hbmqtt.utils.gen_client_id` :param config: Client configuration :param loop: asynio loop to use :return: class instance """ def __init__(self, client_id=None, config=None, loop=None): self.logger = logging.getLogger(__name__) self.config = _defaults if config is not None: self.config.update(config) if client_id is not None: self.client_id = client_id else: from hbmqtt.utils import gen_client_id self.client_id = gen_client_id() self.logger.debug("Using generated client ID : %s" % self.client_id) if loop is not None: self._loop = loop else: self._loop = asyncio.get_event_loop() self.session = None self._handler = None self._disconnect_task = None self._connected_state = asyncio.Event(loop=self._loop) # Init plugins manager context = ClientContext() context.config = self.config self.plugins_manager = PluginManager('hbmqtt.client.plugins', context) self.client_tasks = deque() @asyncio.coroutine def connect(self, uri=None, cleansession=None, cafile=None, capath=None, cadata=None): """ Connect to a remote broker. At first, a network connection is established with the server using the given protocol (``mqtt``, ``mqtts``, ``ws`` or ``wss``). Once the socket is connected, a `CONNECT `_ message is sent with the requested informations. This method is a *coroutine*. :param uri: Broker URI connection, conforming to `MQTT URI scheme `_. Uses ``uri`` config attribute by default. :param cleansession: MQTT CONNECT clean session flag :param cafile: server certificate authority file (optional, used for secured connection) :param capath: server certificate authority path (optional, used for secured connection) :param cadata: server certificate authority data (optional, used for secured connection) :return: `CONNACK `_ return code :raise: :class:`hbmqtt.client.ConnectException` if connection fails """ self.session = self._initsession(uri, cleansession, cafile, capath, cadata) self.logger.debug("Connect to: %s" % uri) try: return (yield from self._do_connect()) except BaseException as be: self.logger.warning("Connection failed: %r" % be) auto_reconnect = self.config.get('auto_reconnect', False) if not auto_reconnect: raise else: return (yield from self.reconnect()) @asyncio.coroutine def disconnect(self): """ Disconnect from the connected broker. This method sends a `DISCONNECT `_ message and closes the network socket. This method is a *coroutine*. """ if self.session.transitions.is_connected(): if not self._disconnect_task.done(): self._disconnect_task.cancel() yield from self._handler.mqtt_disconnect() self._connected_state.clear() yield from self._handler.stop() self.session.transitions.disconnect() else: self.logger.warn("Client session is not currently connected, ignoring call") @asyncio.coroutine def reconnect(self, cleansession=None): """ Reconnect a previously connected broker. Reconnection tries to establish a network connection and send a `CONNECT `_ message. Retries interval and attempts can be controled with the ``reconnect_max_interval`` and ``reconnect_retries`` configuration parameters. This method is a *coroutine*. :param cleansession: clean session flag used in MQTT CONNECT messages sent for reconnections. :return: `CONNACK `_ return code :raise: :class:`hbmqtt.client.ConnectException` if re-connection fails after max retries. """ if self.session.transitions.is_connected(): self.logger.warn("Client already connected") return CONNECTION_ACCEPTED if cleansession: self.session.clean_session = cleansession self.logger.debug("Reconnecting with session parameters: %s" % self.session) reconnect_max_interval = self.config.get('reconnect_max_interval', 10) reconnect_retries = self.config.get('reconnect_retries', 5) nb_attempt = 1 yield from asyncio.sleep(1, loop=self._loop) while True: try: self.logger.debug("Reconnect attempt %d ..." % nb_attempt) return (yield from self._do_connect()) except BaseException as e: self.logger.warning("Reconnection attempt failed: %r" % e) if nb_attempt > reconnect_retries: self.logger.error("Maximum number of connection attempts reached. Reconnection aborted") raise ConnectException("Too many connection attempts failed") exp = 2 ** nb_attempt delay = exp if exp < reconnect_max_interval else reconnect_max_interval self.logger.debug("Waiting %d second before next attempt" % delay) yield from asyncio.sleep(delay, loop=self._loop) nb_attempt += 1 @asyncio.coroutine def _do_connect(self): return_code = yield from self._connect_coro() self._disconnect_task = ensure_future(self.handle_connection_close(), loop=self._loop) return return_code @mqtt_connected @asyncio.coroutine def ping(self): """ Ping the broker. Send a MQTT `PINGREQ `_ message for response. This method is a *coroutine*. """ if self.session.transitions.is_connected(): yield from self._handler.mqtt_ping() else: self.logger.warn("MQTT PING request incompatible with current session state '%s'" % self.session.transitions.state) @mqtt_connected @asyncio.coroutine def publish(self, topic, message, qos=None, retain=None): """ Publish a message to the broker. Send a MQTT `PUBLISH `_ message and wait for acknowledgment depending on Quality Of Service This method is a *coroutine*. :param topic: topic name to which message data is published :param message: payload message (as bytes) to send. :param qos: requested publish quality of service : QOS_0, QOS_1 or QOS_2. Defaults to ``default_qos`` config parameter or QOS_0. :param retain: retain flag. Defaults to ``default_retain`` config parameter or False. """ def get_retain_and_qos(): if qos: assert qos in (QOS_0, QOS_1, QOS_2) _qos = qos else: _qos = self.config['default_qos'] try: _qos = self.config['topics'][topic]['qos'] except KeyError: pass if retain: _retain = retain else: _retain = self.config['default_retain'] try: _retain = self.config['topics'][topic]['retain'] except KeyError: pass return _qos, _retain (app_qos, app_retain) = get_retain_and_qos() return (yield from self._handler.mqtt_publish(topic, message, app_qos, app_retain)) @mqtt_connected @asyncio.coroutine def subscribe(self, topics): """ Subscribe to some topics. Send a MQTT `SUBSCRIBE `_ message and wait for broker acknowledgment. This method is a *coroutine*. :param topics: array of topics pattern to subscribe with associated QoS. :return: `SUBACK `_ message return code. Example of ``topics`` argument expected structure: :: [ ('$SYS/broker/uptime', QOS_1), ('$SYS/broker/load/#', QOS_2), ] """ return (yield from self._handler.mqtt_subscribe(topics, self.session.next_packet_id)) @mqtt_connected @asyncio.coroutine def unsubscribe(self, topics): """ Unsubscribe from some topics. Send a MQTT `UNSUBSCRIBE `_ message and wait for broker `UNSUBACK `_ message. This method is a *coroutine*. :param topics: array of topics to unsubscribe from. Example of ``topics`` argument expected structure: :: ['$SYS/broker/uptime', QOS_1), '$SYS/broker/load/#', QOS_2] """ yield from self._handler.mqtt_unsubscribe(topics, self.session.next_packet_id) @asyncio.coroutine def deliver_message(self, timeout=None): """ Deliver next received message. Deliver next message received from the broker. If no message is available, this methods waits until next message arrives or ``timeout`` occurs. This method is a *coroutine*. :param timeout: maximum number of seconds to wait before returning. If timeout is not specified or None, there is no limit to the wait time until next message arrives. :return: instance of :class:`hbmqtt.session.ApplicationMessage` containing received message information flow. :raises: :class:`asyncio.TimeoutError` if timeout occurs before a message is delivered """ deliver_task = ensure_future(self._handler.mqtt_deliver_next_message(), loop=self._loop) self.client_tasks.append(deliver_task) self.logger.debug("Waiting message delivery") done, pending = yield from asyncio.wait([deliver_task], loop=self._loop, return_when=asyncio.FIRST_EXCEPTION, timeout=timeout) if deliver_task in done: self.client_tasks.pop() return deliver_task.result() else: #timeout occured before message received deliver_task.cancel() raise asyncio.TimeoutError @asyncio.coroutine def _connect_coro(self): kwargs = dict() # Decode URI attributes uri_attributes = urlparse(self.session.broker_uri) scheme = uri_attributes.scheme secure = True if scheme in ('mqtts', 'wss') else False self.session.username = uri_attributes.username self.session.password = uri_attributes.password self.session.remote_address = uri_attributes.hostname self.session.remote_port = uri_attributes.port if scheme in ('mqtt', 'mqtts') and not self.session.remote_port: self.session.remote_port = 8883 if scheme == 'mqtts' else 1883 if scheme in ('ws', 'wss') and not self.session.remote_port: self.session.remote_port = 443 if scheme == 'wss' else 80 if scheme in ('ws', 'wss'): # Rewrite URI to conform to https://tools.ietf.org/html/rfc6455#section-3 uri = (scheme, self.session.remote_address + ":" + str(self.session.remote_port), uri_attributes[2], uri_attributes[3], uri_attributes[4], uri_attributes[5]) self.session.broker_uri = urlunparse(uri) # Init protocol handler #if not self._handler: self._handler = ClientProtocolHandler(self.plugins_manager, loop=self._loop) if secure: if self.session.cafile is None or self.session.cafile == '': self.logger.warn("TLS connection can't be estabilshed, no certificate file (.cert) given") raise ClientException("TLS connection can't be estabilshed, no certificate file (.cert) given") sc = ssl.create_default_context( ssl.Purpose.SERVER_AUTH, cafile=self.session.cafile, capath=self.session.capath, cadata=self.session.cadata) if 'certfile' in self.config and 'keyfile' in self.config: sc.load_cert_chain(self.config['certfile'], self.config['keyfile']) if 'check_hostname' in self.config and isinstance(self.config['check_hostname'], bool): sc.check_hostname = self.config['check_hostname'] kwargs['ssl'] = sc try: reader = None writer = None self._connected_state.clear() # Open connection if scheme in ('mqtt', 'mqtts'): conn_reader, conn_writer = \ yield from asyncio.open_connection( self.session.remote_address, self.session.remote_port, loop=self._loop, **kwargs) reader = StreamReaderAdapter(conn_reader) writer = StreamWriterAdapter(conn_writer) elif scheme in ('ws', 'wss'): websocket = yield from websockets.connect( self.session.broker_uri, subprotocols=['mqtt'], loop=self._loop, **kwargs) reader = WebSocketsReader(websocket) writer = WebSocketsWriter(websocket) # Start MQTT protocol self._handler.attach(self.session, reader, writer) return_code = yield from self._handler.mqtt_connect() if return_code is not CONNECTION_ACCEPTED: self.session.transitions.disconnect() self.logger.warning("Connection rejected with code '%s'" % return_code) exc = ConnectException("Connection rejected by broker") exc.return_code = return_code raise exc else: # Handle MQTT protocol yield from self._handler.start() self.session.transitions.connect() self._connected_state.set() self.logger.debug("connected to %s:%s" % (self.session.remote_address, self.session.remote_port)) return return_code except InvalidURI as iuri: self.logger.warn("connection failed: invalid URI '%s'" % self.session.broker_uri) self.session.transitions.disconnect() raise ConnectException("connection failed: invalid URI '%s'" % self.session.broker_uri, iuri) except InvalidHandshake as ihs: self.logger.warn("connection failed: invalid websocket handshake") self.session.transitions.disconnect() raise ConnectException("connection failed: invalid websocket handshake", ihs) except (ProtocolHandlerException, ConnectionError, OSError) as e: self.logger.warn("MQTT connection failed: %r" % e) self.session.transitions.disconnect() raise ConnectException(e) @asyncio.coroutine def handle_connection_close(self): def cancel_tasks(self): while self.client_tasks: task = self.client_tasks.popleft() if not task.done(): task.set_exception(ClientException("Connection lost")) self.logger.debug("Watch broker disconnection") # Wait for disconnection from broker (like connection lost) yield from self._handler.wait_disconnect() self.logger.warning("Disconnected from broker") # Block client API self._connected_state.clear() # stop an clean handler #yield from self._handler.stop() self._handler.detach() self.session.transitions.disconnect() if self.config.get('auto_reconnect', False): # Try reconnection self.logger.debug("Auto-reconnecting") try: yield from self.reconnect() except ConnectException: # Cancel client pending tasks cancel_tasks(self) else: # Cancel client pending tasks cancel_tasks() def _initsession( self, uri=None, cleansession=None, cafile=None, capath=None, cadata=None) -> Session: # Load config broker_conf = self.config.get('broker', dict()).copy() if uri: broker_conf['uri'] = uri if cafile: broker_conf['cafile'] = cafile elif 'cafile' not in broker_conf: broker_conf['cafile'] = None if capath: broker_conf['capath'] = capath elif 'capath' not in broker_conf: broker_conf['capath'] = None if cadata: broker_conf['cadata'] = cadata elif 'cadata' not in broker_conf: broker_conf['cadata'] = None if cleansession is not None: broker_conf['cleansession'] = cleansession for key in ['uri']: if not_in_dict_or_none(broker_conf, key): raise ClientException("Missing connection parameter '%s'" % key) s = Session() s.broker_uri = uri s.client_id = self.client_id s.cafile = broker_conf['cafile'] s.capath = broker_conf['capath'] s.cadata = broker_conf['cadata'] if cleansession is not None: s.clean_session = cleansession else: s.clean_session = self.config.get('cleansession', True) s.keep_alive = self.config['keep_alive'] - self.config['ping_delay'] if 'will' in self.config: s.will_flag = True s.will_retain = self.config['will']['retain'] s.will_topic = self.config['will']['topic'] s.will_message = self.config['will']['message'] s.will_qos = self.config['will']['qos'] else: s.will_flag = False s.will_retain = False s.will_topic = None s.will_message = None return s hbmqtt-0.9/hbmqtt/codecs.py0000644000076500000240000000642513114237777016070 0ustar nicostaff00000000000000# Copyright (c) 2015 Nicolas JOUANIN # # See the file license.txt for copying permission. import asyncio from struct import pack, unpack from hbmqtt.errors import NoDataException def bytes_to_hex_str(data): """ converts a sequence of bytes into its displayable hex representation, ie: 0x?????? :param data: byte sequence :return: Hexadecimal displayable representation """ return '0x' + ''.join(format(b, '02x') for b in data) def bytes_to_int(data): """ convert a sequence of bytes to an integer using big endian byte ordering :param data: byte sequence :return: integer value """ try: return int.from_bytes(data, byteorder='big') except: return data def int_to_bytes(int_value: int, length: int) -> bytes: """ convert an integer to a sequence of bytes using big endian byte ordering :param int_value: integer value to convert :param length: (optional) byte length :return: byte sequence """ if length == 1: fmt = "!B" elif length == 2: fmt = "!H" return pack(fmt, int_value) @asyncio.coroutine def read_or_raise(reader, n=-1): """ Read a given byte number from Stream. NoDataException is raised if read gives no data :param reader: reader adapter :param n: number of bytes to read :return: bytes read """ data = yield from reader.read(n) if not data: raise NoDataException("No more data") return data @asyncio.coroutine def decode_string(reader) -> bytes: """ Read a string from a reader and decode it according to MQTT string specification :param reader: Stream reader :return: UTF-8 string read from stream """ length_bytes = yield from read_or_raise(reader, 2) str_length = unpack("!H", length_bytes) if str_length[0]: byte_str = yield from read_or_raise(reader, str_length[0]) try: return byte_str.decode(encoding='utf-8') except: return str(byte_str) else: return '' @asyncio.coroutine def decode_data_with_length(reader) -> bytes: """ Read data from a reader. Data is prefixed with 2 bytes length :param reader: Stream reader :return: bytes read from stream (without length) """ length_bytes = yield from read_or_raise(reader, 2) bytes_length = unpack("!H", length_bytes) data = yield from read_or_raise(reader, bytes_length[0]) return data def encode_string(string: str) -> bytes: data = string.encode(encoding='utf-8') data_length = len(data) return int_to_bytes(data_length, 2) + data def encode_data_with_length(data: bytes) -> bytes: data_length = len(data) return int_to_bytes(data_length, 2) + data @asyncio.coroutine def decode_packet_id(reader) -> int: """ Read a packet ID as 2-bytes int from stream according to MQTT specification (2.3.1) :param reader: Stream reader :return: Packet ID """ packet_id_bytes = yield from read_or_raise(reader, 2) packet_id = unpack("!H", packet_id_bytes) return packet_id[0] def int_to_bytes_str(value: int) -> bytes: """ Converts a int value to a bytes array containing the numeric character. Ex: 123 -> b'123' :param value: int value to convert :return: bytes array """ return str(value).encode('utf-8') hbmqtt-0.9/hbmqtt/errors.py0000644000076500000240000000102313114337355016122 0ustar nicostaff00000000000000# Copyright (c) 2015 Nicolas JOUANIN # # See the file license.txt for copying permission. class HBMQTTException(Exception): """ HBMQTT base exception """ pass class MQTTException(Exception): """ Base class for all errors refering to MQTT specifications """ pass class CodecException(Exception): """ Exceptions thrown by packet encode/decode functions """ pass class NoDataException(Exception): """ Exceptions thrown by packet encode/decode functions """ pass hbmqtt-0.9/hbmqtt/mqtt/0000755000076500000240000000000013114340722015215 5ustar nicostaff00000000000000hbmqtt-0.9/hbmqtt/mqtt/__init__.py0000644000076500000240000000271713114237777017354 0ustar nicostaff00000000000000# Copyright (c) 2015 Nicolas JOUANIN # # See the file license.txt for copying permission. from hbmqtt.errors import HBMQTTException from hbmqtt.mqtt.packet import * from hbmqtt.mqtt.connect import ConnectPacket from hbmqtt.mqtt.connack import ConnackPacket from hbmqtt.mqtt.disconnect import DisconnectPacket from hbmqtt.mqtt.pingreq import PingReqPacket from hbmqtt.mqtt.pingresp import PingRespPacket from hbmqtt.mqtt.publish import PublishPacket from hbmqtt.mqtt.puback import PubackPacket from hbmqtt.mqtt.pubrec import PubrecPacket from hbmqtt.mqtt.pubrel import PubrelPacket from hbmqtt.mqtt.pubcomp import PubcompPacket from hbmqtt.mqtt.subscribe import SubscribePacket from hbmqtt.mqtt.suback import SubackPacket from hbmqtt.mqtt.unsubscribe import UnsubscribePacket from hbmqtt.mqtt.unsuback import UnsubackPacket packet_dict = { CONNECT: ConnectPacket, CONNACK: ConnackPacket, PUBLISH: PublishPacket, PUBACK: PubackPacket, PUBREC: PubrecPacket, PUBREL: PubrelPacket, PUBCOMP: PubcompPacket, SUBSCRIBE: SubscribePacket, SUBACK: SubackPacket, UNSUBSCRIBE: UnsubscribePacket, UNSUBACK: UnsubackPacket, PINGREQ: PingReqPacket, PINGRESP: PingRespPacket, DISCONNECT: DisconnectPacket } def packet_class(fixed_header: MQTTFixedHeader): try: cls = packet_dict[fixed_header.packet_type] return cls except KeyError: raise HBMQTTException("Unexpected packet Type '%s'" % fixed_header.packet_type) hbmqtt-0.9/hbmqtt/mqtt/connack.py0000644000076500000240000000526213114237777017227 0ustar nicostaff00000000000000# Copyright (c) 2015 Nicolas JOUANIN # # See the file license.txt for copying permission. import asyncio from hbmqtt.mqtt.packet import CONNACK, MQTTPacket, MQTTFixedHeader, MQTTVariableHeader from hbmqtt.codecs import int_to_bytes, read_or_raise, bytes_to_int from hbmqtt.errors import HBMQTTException from hbmqtt.adapters import ReaderAdapter CONNECTION_ACCEPTED = 0x00 UNACCEPTABLE_PROTOCOL_VERSION = 0x01 IDENTIFIER_REJECTED = 0x02 SERVER_UNAVAILABLE = 0x03 BAD_USERNAME_PASSWORD = 0x04 NOT_AUTHORIZED = 0x05 class ConnackVariableHeader(MQTTVariableHeader): def __init__(self, session_parent=None, return_code=None): super().__init__() self.session_parent = session_parent self.return_code = return_code @classmethod @asyncio.coroutine def from_stream(cls, reader: ReaderAdapter, fixed_header: MQTTFixedHeader): data = yield from read_or_raise(reader, 2) session_parent = data[0] & 0x01 return_code = bytes_to_int(data[1]) return cls(session_parent, return_code) def to_bytes(self): out = bytearray(2) # Connect acknowledge flags if self.session_parent: out[0] = 1 else: out[0] = 0 # return code out[1] = self.return_code return out def __repr__(self): return type(self).__name__ + '(session_parent={0}, return_code={1})'\ .format(hex(self.session_parent), hex(self.return_code)) class ConnackPacket(MQTTPacket): VARIABLE_HEADER = ConnackVariableHeader PAYLOAD = None @property def return_code(self): return self.variable_header.return_code @return_code.setter def return_code(self, return_code): self.variable_header.return_code = return_code @property def session_parent(self): return self.variable_header.session_parent @session_parent.setter def session_parent(self, session_parent): self.variable_header.session_parent = session_parent def __init__(self, fixed: MQTTFixedHeader=None, variable_header: ConnackVariableHeader=None, payload=None): if fixed is None: header = MQTTFixedHeader(CONNACK, 0x00) else: if fixed.packet_type is not CONNACK: raise HBMQTTException("Invalid fixed packet type %s for ConnackPacket init" % fixed.packet_type) header = fixed super().__init__(header) self.variable_header = variable_header self.payload = None @classmethod def build(cls, session_parent=None, return_code=None): v_header = ConnackVariableHeader(session_parent, return_code) packet = ConnackPacket(variable_header=v_header) return packet hbmqtt-0.9/hbmqtt/mqtt/connect.py0000644000076500000240000002273013114237777017243 0ustar nicostaff00000000000000# Copyright (c) 2015 Nicolas JOUANIN # # See the file license.txt for copying permission. from hbmqtt.mqtt.packet import MQTTPacket, MQTTFixedHeader, CONNECT, MQTTVariableHeader, MQTTPayload from hbmqtt.codecs import * from hbmqtt.errors import HBMQTTException, NoDataException from hbmqtt.adapters import ReaderAdapter class ConnectVariableHeader(MQTTVariableHeader): USERNAME_FLAG = 0x80 PASSWORD_FLAG = 0x40 WILL_RETAIN_FLAG = 0x20 WILL_FLAG = 0x04 WILL_QOS_MASK = 0x18 CLEAN_SESSION_FLAG = 0x02 RESERVED_FLAG = 0x01 def __init__(self, connect_flags=0x00, keep_alive=0, proto_name='MQTT', proto_level=0x04): super().__init__() self.proto_name = proto_name self.proto_level = proto_level self.flags = connect_flags self.keep_alive = keep_alive def __repr__(self): return "ConnectVariableHeader(proto_name={0}, proto_level={1}, flags={2}, keepalive={3})".format( self.proto_name, self.proto_level, hex(self.flags), self.keep_alive) def _set_flag(self, val, mask): if val: self.flags |= mask else: self.flags &= ~mask def _get_flag(self, mask): if self.flags & mask: return True else: return False @property def username_flag(self) -> bool: return self._get_flag(self.USERNAME_FLAG) @username_flag.setter def username_flag(self, val: bool): self._set_flag(val, self.USERNAME_FLAG) @property def password_flag(self) -> bool: return self._get_flag(self.PASSWORD_FLAG) @password_flag.setter def password_flag(self, val: bool): self._set_flag(val, self.PASSWORD_FLAG) @property def will_retain_flag(self) -> bool: return self._get_flag(self.WILL_RETAIN_FLAG) @will_retain_flag.setter def will_retain_flag(self, val: bool): self._set_flag(val, self.WILL_RETAIN_FLAG) @property def will_flag(self) -> bool: return self._get_flag(self.WILL_FLAG) @will_flag.setter def will_flag(self, val: bool): self._set_flag(val, self.WILL_FLAG) @property def clean_session_flag(self) -> bool: return self._get_flag(self.CLEAN_SESSION_FLAG) @clean_session_flag.setter def clean_session_flag(self, val: bool): self._set_flag(val, self.CLEAN_SESSION_FLAG) @property def reserved_flag(self) -> bool: return self._get_flag(self.RESERVED_FLAG) @property def will_qos(self): return (self.flags & 0x18) >> 3 @will_qos.setter def will_qos(self, val: int): self.flags &= 0xe7 # Reset QOS flags self.flags |= (val << 3) @classmethod @asyncio.coroutine def from_stream(cls, reader: ReaderAdapter, fixed_header: MQTTFixedHeader): # protocol name protocol_name = yield from decode_string(reader) # protocol level protocol_level_byte = yield from read_or_raise(reader, 1) protocol_level = bytes_to_int(protocol_level_byte) # flags flags_byte = yield from read_or_raise(reader, 1) flags = bytes_to_int(flags_byte) # keep-alive keep_alive_byte = yield from read_or_raise(reader, 2) keep_alive = bytes_to_int(keep_alive_byte) return cls(flags, keep_alive, protocol_name, protocol_level) def to_bytes(self): out = bytearray() # Protocol name out.extend(encode_string(self.proto_name)) # Protocol level out.append(self.proto_level) # flags out.append(self.flags) # keep alive out.extend(int_to_bytes(self.keep_alive, 2)) return out class ConnectPayload(MQTTPayload): def __init__(self, client_id=None, will_topic=None, will_message=None, username=None, password=None): super().__init__() self.client_id = client_id self.will_topic = will_topic self.will_message = will_message self.username = username self.password = password def __repr__(self): return "ConnectVariableHeader(client_id={0}, will_topic={1}, will_message={2}, username={3}, password={4})".\ format(self.client_id, self.will_topic, self.will_message, self.username, self.password) @classmethod @asyncio.coroutine def from_stream(cls, reader: ReaderAdapter, fixed_header: MQTTFixedHeader, variable_header: ConnectVariableHeader): payload = cls() # Client identifier try: payload.client_id = yield from decode_string(reader) except NoDataException: payload.client_id = None # Read will topic, username and password if variable_header.will_flag: try: payload.will_topic = yield from decode_string(reader) payload.will_message = yield from decode_data_with_length(reader) except NoDataException: payload.will_topic = None payload.will_message = None if variable_header.username_flag: try: payload.username = yield from decode_string(reader) except NoDataException: payload.username = None if variable_header.password_flag: try: payload.password = yield from decode_string(reader) except NoDataException: payload.password = None return payload def to_bytes(self, fixed_header: MQTTFixedHeader, variable_header: ConnectVariableHeader): out = bytearray() # Client identifier out.extend(encode_string(self.client_id)) # Will topic / message if variable_header.will_flag: out.extend(encode_string(self.will_topic)) out.extend(encode_data_with_length(self.will_message)) # username if variable_header.username_flag: out.extend(encode_string(self.username)) # password if variable_header.password_flag: out.extend(encode_string(self.password)) return out class ConnectPacket(MQTTPacket): VARIABLE_HEADER = ConnectVariableHeader PAYLOAD = ConnectPayload @property def proto_name(self): return self.variable_header.proto_name @proto_name.setter def proto_name(self, name: str): self.variable_header.proto_name = name @property def proto_level(self): return self.variable_header.proto_level @proto_level.setter def proto_level(self, level): self.variable_header.proto_level = level @property def username_flag(self): return self.variable_header.username_flag @username_flag.setter def username_flag(self, flag): self.variable_header.username_flag = flag @property def password_flag(self): return self.variable_header.password_flag @password_flag.setter def password_flag(self, flag): self.variable_header.password_flag = flag @property def clean_session_flag(self): return self.variable_header.clean_session_flag @clean_session_flag.setter def clean_session_flag(self, flag): self.variable_header.clean_session_flag = flag @property def will_retain_flag(self): return self.variable_header.will_retain_flag @will_retain_flag.setter def will_retain_flag(self, flag): self.variable_header.will_retain_flag = flag @property def will_qos(self): return self.variable_header.will_qos @will_qos.setter def will_qos(self, flag): self.variable_header.will_qos = flag @property def will_flag(self): return self.variable_header.will_flag @will_flag.setter def will_flag(self, flag): self.variable_header.will_flag = flag @property def reserved_flag(self): return self.variable_header.reserved_flag @reserved_flag.setter def reserved_flag(self, flag): self.variable_header.reserved_flag = flag @property def client_id(self): return self.payload.client_id @client_id.setter def client_id(self, client_id): self.payload.client_id = client_id @property def will_topic(self): return self.payload.will_topic @will_topic.setter def will_topic(self, will_topic): self.payload.will_topic = will_topic @property def will_message(self): return self.payload.will_message @will_message.setter def will_message(self, will_message): self.payload.will_message = will_message @property def username(self): return self.payload.username @username.setter def username(self, username): self.payload.username = username @property def password(self): return self.payload.password @password.setter def password(self, password): self.payload.password = password @property def keep_alive(self): return self.variable_header.keep_alive @keep_alive.setter def keep_alive(self, keep_alive): self.variable_header.keep_alive = keep_alive def __init__(self, fixed: MQTTFixedHeader=None, vh: ConnectVariableHeader=None, payload: ConnectPayload=None): if fixed is None: header = MQTTFixedHeader(CONNECT, 0x00) else: if fixed.packet_type is not CONNECT: raise HBMQTTException("Invalid fixed packet type %s for ConnectPacket init" % fixed.packet_type) header = fixed super().__init__(header) self.variable_header = vh self.payload = payload hbmqtt-0.9/hbmqtt/mqtt/constants.py0000644000076500000240000000020213114237777017614 0ustar nicostaff00000000000000# Copyright (c) 2015 Nicolas JOUANIN # # See the file license.txt for copying permission. QOS_0 = 0x00 QOS_1 = 0x01 QOS_2 = 0x02 hbmqtt-0.9/hbmqtt/mqtt/disconnect.py0000644000076500000240000000133013114237777017734 0ustar nicostaff00000000000000# Copyright (c) 2015 Nicolas JOUANIN # # See the file license.txt for copying permission. from hbmqtt.mqtt.packet import MQTTPacket, MQTTFixedHeader, DISCONNECT from hbmqtt.errors import HBMQTTException class DisconnectPacket(MQTTPacket): VARIABLE_HEADER = None PAYLOAD = None def __init__(self, fixed: MQTTFixedHeader=None): if fixed is None: header = MQTTFixedHeader(DISCONNECT, 0x00) else: if fixed.packet_type is not DISCONNECT: raise HBMQTTException("Invalid fixed packet type %s for DisconnectPacket init" % fixed.packet_type) header = fixed super().__init__(header) self.variable_header = None self.payload = None hbmqtt-0.9/hbmqtt/mqtt/packet.py0000644000076500000240000001637013114237777017064 0ustar nicostaff00000000000000# Copyright (c) 2015 Nicolas JOUANIN # # See the file license.txt for copying permission. from hbmqtt.errors import CodecException, MQTTException from hbmqtt.codecs import * from hbmqtt.adapters import ReaderAdapter, WriterAdapter from datetime import datetime from struct import unpack RESERVED_0 = 0x00 CONNECT = 0x01 CONNACK = 0x02 PUBLISH = 0x03 PUBACK = 0x04 PUBREC = 0x05 PUBREL = 0x06 PUBCOMP = 0x07 SUBSCRIBE = 0x08 SUBACK = 0x09 UNSUBSCRIBE = 0x0a UNSUBACK = 0x0b PINGREQ = 0x0c PINGRESP = 0x0d DISCONNECT = 0x0e RESERVED_15 = 0x0f class MQTTFixedHeader: def __init__(self, packet_type, flags=0, length=0): self.packet_type = packet_type self.remaining_length = length self.flags = flags def to_bytes(self): def encode_remaining_length(length: int): encoded = bytearray() while True: length_byte = length % 0x80 length //= 0x80 if length > 0: length_byte |= 0x80 encoded.append(length_byte) if length <= 0: break return encoded out = bytearray() packet_type = 0 try: packet_type = (self.packet_type << 4) | self.flags out.append(packet_type) except OverflowError: raise CodecException('packet_type encoding exceed 1 byte length: value=%d', packet_type) encoded_length = encode_remaining_length(self.remaining_length) out.extend(encoded_length) return out @asyncio.coroutine def to_stream(self, writer: WriterAdapter): writer.write(self.to_bytes()) @property def bytes_length(self): return len(self.to_bytes()) @classmethod @asyncio.coroutine def from_stream(cls, reader: ReaderAdapter): """ Read and decode MQTT message fixed header from stream :return: FixedHeader instance """ @asyncio.coroutine def decode_remaining_length(): """ Decode message length according to MQTT specifications :return: """ multiplier = 1 value = 0 buffer = bytearray() while True: encoded_byte = yield from reader.read(1) int_byte = unpack('!B', encoded_byte) buffer.append(int_byte[0]) value += (int_byte[0] & 0x7f) * multiplier if (int_byte[0] & 0x80) == 0: break else: multiplier *= 128 if multiplier > 128 * 128 * 128: raise MQTTException("Invalid remaining length bytes:%s, packet_type=%d" % (bytes_to_hex_str(buffer), msg_type)) return value try: byte1 = yield from read_or_raise(reader, 1) int1 = unpack('!B', byte1) msg_type = (int1[0] & 0xf0) >> 4 flags = int1[0] & 0x0f remain_length = yield from decode_remaining_length() return cls(msg_type, flags, remain_length) except NoDataException: return None def __repr__(self): return type(self).__name__ + '(length={0}, flags={1})'.\ format(self.remaining_length, hex(self.flags)) class MQTTVariableHeader: def __init__(self): pass @asyncio.coroutine def to_stream(self, writer: asyncio.StreamWriter): writer.write(self.to_bytes()) yield from writer.drain() def to_bytes(self) -> bytes: """ Serialize header data to a byte array conforming to MQTT protocol :return: serialized data """ @property def bytes_length(self): return len(self.to_bytes()) @classmethod @asyncio.coroutine def from_stream(cls, reader: asyncio.StreamReader, fixed_header: MQTTFixedHeader): pass class PacketIdVariableHeader(MQTTVariableHeader): def __init__(self, packet_id): super().__init__() self.packet_id = packet_id def to_bytes(self): out = b'' out += int_to_bytes(self.packet_id, 2) return out @classmethod @asyncio.coroutine def from_stream(cls, reader: ReaderAdapter, fixed_header: MQTTFixedHeader): packet_id = yield from decode_packet_id(reader) return cls(packet_id) def __repr__(self): return type(self).__name__ + '(packet_id={0})'.format(self.packet_id) class MQTTPayload: def __init__(self): pass @asyncio.coroutine def to_stream(self, writer: asyncio.StreamWriter): writer.write(self.to_bytes()) yield from writer.drain() def to_bytes(self, fixed_header: MQTTFixedHeader, variable_header: MQTTVariableHeader): pass @classmethod @asyncio.coroutine def from_stream(cls, reader: asyncio.StreamReader, fixed_header: MQTTFixedHeader, variable_header: MQTTVariableHeader): pass class MQTTPacket: FIXED_HEADER = MQTTFixedHeader VARIABLE_HEADER = None PAYLOAD = None def __init__(self, fixed: MQTTFixedHeader, variable_header: MQTTVariableHeader=None, payload: MQTTPayload=None): self.fixed_header = fixed self.variable_header = variable_header self.payload = payload self.protocol_ts = None @asyncio.coroutine def to_stream(self, writer: asyncio.StreamWriter): writer.write(self.to_bytes()) yield from writer.drain() self.protocol_ts = datetime.now() def to_bytes(self) -> bytes: if self.variable_header: variable_header_bytes = self.variable_header.to_bytes() else: variable_header_bytes = b'' if self.payload: payload_bytes = self.payload.to_bytes(self.fixed_header, self.variable_header) else: payload_bytes = b'' self.fixed_header.remaining_length = len(variable_header_bytes) + len(payload_bytes) fixed_header_bytes = self.fixed_header.to_bytes() return fixed_header_bytes + variable_header_bytes + payload_bytes @classmethod @asyncio.coroutine def from_stream(cls, reader: ReaderAdapter, fixed_header=None, variable_header=None): if fixed_header is None: fixed_header = yield from cls.FIXED_HEADER.from_stream(reader) if cls.VARIABLE_HEADER: if variable_header is None: variable_header = yield from cls.VARIABLE_HEADER.from_stream(reader, fixed_header) else: variable_header = None if cls.PAYLOAD: payload = yield from cls.PAYLOAD.from_stream(reader, fixed_header, variable_header) else: payload = None if fixed_header and not variable_header and not payload: instance = cls(fixed_header) elif fixed_header and not payload: instance = cls(fixed_header, variable_header) else: instance = cls(fixed_header, variable_header, payload) instance.protocol_ts = datetime.now() return instance @property def bytes_length(self): return len(self.to_bytes()) def __repr__(self): return type(self).__name__ + '(ts={0!s}, fixed={1!r}, variable={2!r}, payload={3!r})'.\ format(self.protocol_ts, self.fixed_header, self.variable_header, self.payload) hbmqtt-0.9/hbmqtt/mqtt/pingreq.py0000644000076500000240000000131113114237777017247 0ustar nicostaff00000000000000# Copyright (c) 2015 Nicolas JOUANIN # # See the file license.txt for copying permission. from hbmqtt.mqtt.packet import MQTTPacket, MQTTFixedHeader, PINGREQ from hbmqtt.errors import HBMQTTException class PingReqPacket(MQTTPacket): VARIABLE_HEADER = None PAYLOAD = None def __init__(self, fixed: MQTTFixedHeader=None): if fixed is None: header = MQTTFixedHeader(PINGREQ, 0x00) else: if fixed.packet_type is not PINGREQ: raise HBMQTTException("Invalid fixed packet type %s for PingReqPacket init" % fixed.packet_type) header = fixed super().__init__(header) self.variable_header = None self.payload = None hbmqtt-0.9/hbmqtt/mqtt/pingresp.py0000644000076500000240000000141113114237777017432 0ustar nicostaff00000000000000# Copyright (c) 2015 Nicolas JOUANIN # # See the file license.txt for copying permission. from hbmqtt.mqtt.packet import MQTTPacket, MQTTFixedHeader, PINGRESP from hbmqtt.errors import HBMQTTException class PingRespPacket(MQTTPacket): VARIABLE_HEADER = None PAYLOAD = None def __init__(self, fixed: MQTTFixedHeader=None): if fixed is None: header = MQTTFixedHeader(PINGRESP, 0x00) else: if fixed.packet_type is not PINGRESP: raise HBMQTTException("Invalid fixed packet type %s for PingRespPacket init" % fixed.packet_type) header = fixed super().__init__(header) self.variable_header = None self.payload = None @classmethod def build(cls): return cls() hbmqtt-0.9/hbmqtt/mqtt/protocol/0000755000076500000240000000000013114340722017056 5ustar nicostaff00000000000000hbmqtt-0.9/hbmqtt/mqtt/protocol/__init__.py0000644000076500000240000000000013114237777021174 0ustar nicostaff00000000000000hbmqtt-0.9/hbmqtt/mqtt/protocol/broker_handler.py0000644000076500000240000002065413114237777022437 0ustar nicostaff00000000000000# Copyright (c) 2015 Nicolas JOUANIN # # See the file license.txt for copying permission. import asyncio from asyncio import futures, Queue from hbmqtt.mqtt.protocol.handler import ProtocolHandler from hbmqtt.mqtt.connect import ConnectPacket from hbmqtt.mqtt.connack import * from hbmqtt.mqtt.pingreq import PingReqPacket from hbmqtt.mqtt.pingresp import PingRespPacket from hbmqtt.mqtt.subscribe import SubscribePacket from hbmqtt.mqtt.suback import SubackPacket from hbmqtt.mqtt.unsubscribe import UnsubscribePacket from hbmqtt.mqtt.unsuback import UnsubackPacket from hbmqtt.utils import format_client_message from hbmqtt.session import Session from hbmqtt.plugins.manager import PluginManager from hbmqtt.adapters import ReaderAdapter, WriterAdapter from hbmqtt.errors import MQTTException from .handler import EVENT_MQTT_PACKET_RECEIVED, EVENT_MQTT_PACKET_SENT class BrokerProtocolHandler(ProtocolHandler): def __init__(self, plugins_manager: PluginManager, session: Session=None, loop=None): super().__init__(plugins_manager, session, loop) self._disconnect_waiter = None self._pending_subscriptions = Queue(loop=self._loop) self._pending_unsubscriptions = Queue(loop=self._loop) @asyncio.coroutine def start(self): yield from super().start() if self._disconnect_waiter is None: self._disconnect_waiter = futures.Future(loop=self._loop) @asyncio.coroutine def stop(self): yield from super().stop() if self._disconnect_waiter is not None and not self._disconnect_waiter.done(): self._disconnect_waiter.set_result(None) @asyncio.coroutine def wait_disconnect(self): return (yield from self._disconnect_waiter) def handle_write_timeout(self): pass def handle_read_timeout(self): if self._disconnect_waiter is not None and not self._disconnect_waiter.done(): self._disconnect_waiter.set_result(None) @asyncio.coroutine def handle_disconnect(self, disconnect): self.logger.debug("Client disconnecting") if self._disconnect_waiter and not self._disconnect_waiter.done(): self.logger.debug("Setting waiter result to %r" % disconnect) self._disconnect_waiter.set_result(disconnect) @asyncio.coroutine def handle_connection_closed(self): yield from self.handle_disconnect(None) @asyncio.coroutine def handle_connect(self, connect: ConnectPacket): # Broker handler shouldn't received CONNECT message during messages handling # as CONNECT messages are managed by the broker on client connection self.logger.error('%s [MQTT-3.1.0-2] %s : CONNECT message received during messages handling' % (self.session.client_id, format_client_message(self.session))) if self._disconnect_waiter is not None and not self._disconnect_waiter.done(): self._disconnect_waiter.set_result(None) @asyncio.coroutine def handle_pingreq(self, pingreq: PingReqPacket): yield from self._send_packet(PingRespPacket.build()) @asyncio.coroutine def handle_subscribe(self, subscribe: SubscribePacket): subscription = {'packet_id': subscribe.variable_header.packet_id, 'topics': subscribe.payload.topics} yield from self._pending_subscriptions.put(subscription) @asyncio.coroutine def handle_unsubscribe(self, unsubscribe: UnsubscribePacket): unsubscription = {'packet_id': unsubscribe.variable_header.packet_id, 'topics': unsubscribe.payload.topics} yield from self._pending_unsubscriptions.put(unsubscription) @asyncio.coroutine def get_next_pending_subscription(self): subscription = yield from self._pending_subscriptions.get() return subscription @asyncio.coroutine def get_next_pending_unsubscription(self): unsubscription = yield from self._pending_unsubscriptions.get() return unsubscription @asyncio.coroutine def mqtt_acknowledge_subscription(self, packet_id, return_codes): suback = SubackPacket.build(packet_id, return_codes) yield from self._send_packet(suback) @asyncio.coroutine def mqtt_acknowledge_unsubscription(self, packet_id): unsuback = UnsubackPacket.build(packet_id) yield from self._send_packet(unsuback) @asyncio.coroutine def mqtt_connack_authorize(self, authorize: bool): if authorize: connack = ConnackPacket.build(self.session.parent, CONNECTION_ACCEPTED) else: connack = ConnackPacket.build(self.session.parent, NOT_AUTHORIZED) yield from self._send_packet(connack) @classmethod @asyncio.coroutine def init_from_connect(cls, reader: ReaderAdapter, writer: WriterAdapter, plugins_manager, loop=None): """ :param reader: :param writer: :param plugins_manager: :param loop: :return: """ remote_address, remote_port = writer.get_peer_info() connect = yield from ConnectPacket.from_stream(reader) yield from plugins_manager.fire_event(EVENT_MQTT_PACKET_RECEIVED, packet=connect) if connect.payload.client_id is None: raise MQTTException('[[MQTT-3.1.3-3]] : Client identifier must be present' ) if connect.variable_header.will_flag: if connect.payload.will_topic is None or connect.payload.will_message is None: raise MQTTException('will flag set, but will topic/message not present in payload') if connect.variable_header.reserved_flag: raise MQTTException('[MQTT-3.1.2-3] CONNECT reserved flag must be set to 0') if connect.proto_name != "MQTT": raise MQTTException('[MQTT-3.1.2-1] Incorrect protocol name: "%s"' % connect.proto_name) connack = None error_msg = None if connect.proto_level != 4: # only MQTT 3.1.1 supported error_msg = 'Invalid protocol from %s: %d' % \ (format_client_message(address=remote_address, port=remote_port), connect.proto_level) connack = ConnackPacket.build(0, UNACCEPTABLE_PROTOCOL_VERSION) # [MQTT-3.2.2-4] session_parent=0 elif not connect.username_flag and connect.password_flag: connack = ConnackPacket.build(0, BAD_USERNAME_PASSWORD) # [MQTT-3.1.2-22] elif connect.username_flag and not connect.password_flag: connack = ConnackPacket.build(0, BAD_USERNAME_PASSWORD) # [MQTT-3.1.2-22] elif connect.username_flag and connect.username is None: error_msg = 'Invalid username from %s' % \ (format_client_message(address=remote_address, port=remote_port)) connack = ConnackPacket.build(0, BAD_USERNAME_PASSWORD) # [MQTT-3.2.2-4] session_parent=0 elif connect.password_flag and connect.password is None: error_msg = 'Invalid password %s' % (format_client_message(address=remote_address, port=remote_port)) connack = ConnackPacket.build(0, BAD_USERNAME_PASSWORD) # [MQTT-3.2.2-4] session_parent=0 elif connect.clean_session_flag is False and (connect.payload.client_id is None or connect.payload.client_id == ""): error_msg = '[MQTT-3.1.3-8] [MQTT-3.1.3-9] %s: No client Id provided (cleansession=0)' % \ format_client_message(address=remote_address, port=remote_port) connack = ConnackPacket.build(0, IDENTIFIER_REJECTED) if connack is not None: yield from plugins_manager.fire_event(EVENT_MQTT_PACKET_SENT, packet=connack) yield from connack.to_stream(writer) yield from writer.close() raise MQTTException(error_msg) incoming_session = Session(loop) incoming_session.client_id = connect.client_id incoming_session.clean_session = connect.clean_session_flag incoming_session.will_flag = connect.will_flag incoming_session.will_retain = connect.will_retain_flag incoming_session.will_qos = connect.will_qos incoming_session.will_topic = connect.will_topic incoming_session.will_message = connect.will_message incoming_session.username = connect.username incoming_session.password = connect.password if connect.keep_alive > 0: incoming_session.keep_alive = connect.keep_alive else: incoming_session.keep_alive = 0 handler = cls(plugins_manager, loop=loop) return handler, incoming_session hbmqtt-0.9/hbmqtt/mqtt/protocol/client_handler.py0000644000076500000240000001470113114237777022425 0ustar nicostaff00000000000000# Copyright (c) 2015 Nicolas JOUANIN # # See the file license.txt for copying permission. import asyncio from asyncio import futures import sys if sys.version_info < (3, 5): from asyncio import async as ensure_future else: from asyncio import ensure_future from hbmqtt.mqtt.protocol.handler import ProtocolHandler, EVENT_MQTT_PACKET_RECEIVED from hbmqtt.mqtt.packet import * from hbmqtt.mqtt.disconnect import DisconnectPacket from hbmqtt.mqtt.pingreq import PingReqPacket from hbmqtt.mqtt.pingresp import PingRespPacket from hbmqtt.mqtt.subscribe import SubscribePacket from hbmqtt.mqtt.suback import SubackPacket from hbmqtt.mqtt.unsubscribe import UnsubscribePacket from hbmqtt.mqtt.unsuback import UnsubackPacket from hbmqtt.mqtt.connect import ConnectVariableHeader, ConnectPayload, ConnectPacket from hbmqtt.mqtt.connack import ConnackPacket from hbmqtt.session import Session from hbmqtt.plugins.manager import PluginManager class ClientProtocolHandler(ProtocolHandler): def __init__(self, plugins_manager: PluginManager, session: Session=None, loop=None): super().__init__(plugins_manager, session, loop=loop) self._ping_task = None self._pingresp_queue = asyncio.Queue(loop=self._loop) self._subscriptions_waiter = dict() self._unsubscriptions_waiter = dict() self._disconnect_waiter = None @asyncio.coroutine def start(self): yield from super().start() if self._disconnect_waiter is None: self._disconnect_waiter = futures.Future(loop=self._loop) @asyncio.coroutine def stop(self): yield from super().stop() if self._ping_task: try: self.logger.debug("Cancel ping task") self._ping_task.cancel() except BaseException: pass if not self._disconnect_waiter.done(): self._disconnect_waiter.cancel() self._disconnect_waiter = None def _build_connect_packet(self): vh = ConnectVariableHeader() payload = ConnectPayload() vh.keep_alive = self.session.keep_alive vh.clean_session_flag = self.session.clean_session vh.will_retain_flag = self.session.will_retain payload.client_id = self.session.client_id if self.session.username: vh.username_flag = True payload.username = self.session.username else: vh.username_flag = False if self.session.password: vh.password_flag = True payload.password = self.session.password else: vh.password_flag = False if self.session.will_flag: vh.will_flag = True vh.will_qos = self.session.will_qos payload.will_message = self.session.will_message payload.will_topic = self.session.will_topic else: vh.will_flag = False packet = ConnectPacket(vh=vh, payload=payload) return packet @asyncio.coroutine def mqtt_connect(self): connect_packet = self._build_connect_packet() yield from self._send_packet(connect_packet) connack = yield from ConnackPacket.from_stream(self.reader) yield from self.plugins_manager.fire_event(EVENT_MQTT_PACKET_RECEIVED, packet=connack, session=self.session) return connack.return_code def handle_write_timeout(self): try: if not self._ping_task: self.logger.debug("Scheduling Ping") self._ping_task = ensure_future(self.mqtt_ping()) except BaseException as be: self.logger.debug("Exception ignored in ping task: %r" % be) def handle_read_timeout(self): pass @asyncio.coroutine def mqtt_subscribe(self, topics, packet_id): """ :param topics: array of topics [{'filter':'/a/b', 'qos': 0x00}, ...] :return: """ # Build and send SUBSCRIBE message subscribe = SubscribePacket.build(topics, packet_id) yield from self._send_packet(subscribe) # Wait for SUBACK is received waiter = futures.Future(loop=self._loop) self._subscriptions_waiter[subscribe.variable_header.packet_id] = waiter return_codes = yield from waiter del self._subscriptions_waiter[subscribe.variable_header.packet_id] return return_codes @asyncio.coroutine def handle_suback(self, suback: SubackPacket): packet_id = suback.variable_header.packet_id try: waiter = self._subscriptions_waiter.get(packet_id) waiter.set_result(suback.payload.return_codes) except KeyError as ke: self.logger.warning("Received SUBACK for unknown pending subscription with Id: %s" % packet_id) @asyncio.coroutine def mqtt_unsubscribe(self, topics, packet_id): """ :param topics: array of topics ['/a/b', ...] :return: """ unsubscribe = UnsubscribePacket.build(topics, packet_id) yield from self._send_packet(unsubscribe) waiter = futures.Future(loop=self._loop) self._unsubscriptions_waiter[unsubscribe.variable_header.packet_id] = waiter yield from waiter del self._unsubscriptions_waiter[unsubscribe.variable_header.packet_id] @asyncio.coroutine def handle_unsuback(self, unsuback: UnsubackPacket): packet_id = unsuback.variable_header.packet_id try: waiter = self._unsubscriptions_waiter.get(packet_id) waiter.set_result(None) except KeyError as ke: self.logger.warning("Received UNSUBACK for unknown pending subscription with Id: %s" % packet_id) @asyncio.coroutine def mqtt_disconnect(self): disconnect_packet = DisconnectPacket() yield from self._send_packet(disconnect_packet) @asyncio.coroutine def mqtt_ping(self): ping_packet = PingReqPacket() yield from self._send_packet(ping_packet) resp = yield from self._pingresp_queue.get() if self._ping_task: self._ping_task = None return resp @asyncio.coroutine def handle_pingresp(self, pingresp: PingRespPacket): yield from self._pingresp_queue.put(pingresp) @asyncio.coroutine def handle_connection_closed(self): self.logger.debug("Broker closed connection") if not self._disconnect_waiter.done(): self._disconnect_waiter.set_result(None) @asyncio.coroutine def wait_disconnect(self): yield from self._disconnect_waiter hbmqtt-0.9/hbmqtt/mqtt/protocol/handler.py0000644000076500000240000006407513114237777021100 0ustar nicostaff00000000000000# Copyright (c) 2015 Nicolas JOUANIN # # See the file license.txt for copying permission. import logging import collections import itertools from asyncio import InvalidStateError from hbmqtt.mqtt import packet_class from hbmqtt.mqtt.packet import * from hbmqtt.mqtt.connack import ConnackPacket from hbmqtt.mqtt.connect import ConnectPacket from hbmqtt.mqtt.pingresp import PingRespPacket from hbmqtt.mqtt.pingreq import PingReqPacket from hbmqtt.mqtt.publish import PublishPacket from hbmqtt.mqtt.pubrel import PubrelPacket from hbmqtt.mqtt.puback import PubackPacket from hbmqtt.mqtt.pubrec import PubrecPacket from hbmqtt.mqtt.pubcomp import PubcompPacket from hbmqtt.mqtt.suback import SubackPacket from hbmqtt.mqtt.subscribe import SubscribePacket from hbmqtt.mqtt.unsubscribe import UnsubscribePacket from hbmqtt.mqtt.unsuback import UnsubackPacket from hbmqtt.mqtt.disconnect import DisconnectPacket from hbmqtt.adapters import ReaderAdapter, WriterAdapter from hbmqtt.session import Session, OutgoingApplicationMessage, IncomingApplicationMessage, INCOMING, OUTGOING from hbmqtt.mqtt.constants import * from hbmqtt.plugins.manager import PluginManager from hbmqtt.errors import HBMQTTException import sys if sys.version_info < (3, 5): from asyncio import async as ensure_future else: from asyncio import ensure_future EVENT_MQTT_PACKET_SENT = 'mqtt_packet_sent' EVENT_MQTT_PACKET_RECEIVED = 'mqtt_packet_received' class ProtocolHandlerException(BaseException): pass class ProtocolHandler: """ Class implementing the MQTT communication protocol using asyncio features """ def __init__(self, plugins_manager: PluginManager, session: Session=None, loop=None): self.logger = logging.getLogger(__name__) if session: self._init_session(session) else: self.session = None self.reader = None self.writer = None self.plugins_manager = plugins_manager if loop is None: self._loop = asyncio.get_event_loop() else: self._loop = loop self._reader_task = None self._keepalive_task = None self._reader_ready = None self._reader_stopped = asyncio.Event(loop=self._loop) self._puback_waiters = dict() self._pubrec_waiters = dict() self._pubrel_waiters = dict() self._pubcomp_waiters = dict() def _init_session(self, session: Session): assert session log = logging.getLogger(__name__) self.session = session self.logger = logging.LoggerAdapter(log, {'client_id': self.session.client_id}) self.keepalive_timeout = self.session.keep_alive if self.keepalive_timeout <= 0: self.keepalive_timeout = None def attach(self, session, reader: ReaderAdapter, writer: WriterAdapter): if self.session: raise ProtocolHandlerException("Handler is already attached to a session") self._init_session(session) self.reader = reader self.writer = writer def detach(self): self.session = None self.reader = None self.writer = None def _is_attached(self): if self.session: return True else: return False @asyncio.coroutine def start(self): if not self._is_attached(): raise ProtocolHandlerException("Handler is not attached to a stream") self._reader_ready = asyncio.Event(loop=self._loop) self._reader_task = asyncio.Task(self._reader_loop(), loop=self._loop) yield from asyncio.wait([self._reader_ready.wait()], loop=self._loop) if self.keepalive_timeout: self._keepalive_task = self._loop.call_later(self.keepalive_timeout, self.handle_write_timeout) self.logger.debug("Handler tasks started") yield from self._retry_deliveries() self.logger.debug("Handler ready") @asyncio.coroutine def stop(self): # Stop messages flow waiter self._stop_waiters() if self._keepalive_task: self._keepalive_task.cancel() self.logger.debug("waiting for tasks to be stopped") if not self._reader_task.done(): self._reader_task.cancel() yield from asyncio.wait( [self._reader_stopped.wait()], loop=self._loop) self.logger.debug("closing writer") try: yield from self.writer.close() except Exception as e: self.logger.debug("Handler writer close failed: %s" % e) def _stop_waiters(self): self.logger.debug("Stopping %d puback waiters" % len(self._puback_waiters)) self.logger.debug("Stopping %d pucomp waiters" % len(self._pubcomp_waiters)) self.logger.debug("Stopping %d purec waiters" % len(self._pubrec_waiters)) self.logger.debug("Stopping %d purel waiters" % len(self._pubrel_waiters)) for waiter in itertools.chain( self._puback_waiters.values(), self._pubcomp_waiters.values(), self._pubrec_waiters.values(), self._pubrel_waiters.values()): waiter.cancel() @asyncio.coroutine def _retry_deliveries(self): """ Handle [MQTT-4.4.0-1] by resending PUBLISH and PUBREL messages for pending out messages :return: """ self.logger.debug("Begin messages delivery retries") tasks = [] for message in itertools.chain(self.session.inflight_in.values(), self.session.inflight_out.values()): tasks.append(asyncio.wait_for(self._handle_message_flow(message), 10, loop=self._loop)) if tasks: done, pending = yield from asyncio.wait(tasks, loop=self._loop) self.logger.debug("%d messages redelivered" % len(done)) self.logger.debug("%d messages not redelivered due to timeout" % len(pending)) self.logger.debug("End messages delivery retries") @asyncio.coroutine def mqtt_publish(self, topic, data, qos, retain, ack_timeout=None): """ Sends a MQTT publish message and manages messages flows. This methods doesn't return until the message has been acknowledged by receiver or timeout occur :param topic: MQTT topic to publish :param data: data to send on topic :param qos: quality of service to use for message flow. Can be QOS_0, QOS_1 or QOS_2 :param retain: retain message flag :param ack_timeout: acknowledge timeout. If set, this method will return a TimeOut error if the acknowledgment is not completed before ack_timeout second :return: ApplicationMessage used during inflight operations """ if qos in (QOS_1, QOS_2): packet_id = self.session.next_packet_id if packet_id in self.session.inflight_out: raise HBMQTTException("A message with the same packet ID '%d' is already in flight" % packet_id) else: packet_id = None message = OutgoingApplicationMessage(packet_id, topic, qos, data, retain) # Handle message flow if ack_timeout is not None and ack_timeout > 0: yield from asyncio.wait_for(self._handle_message_flow(message), ack_timeout, loop=self._loop) else: yield from self._handle_message_flow(message) return message @asyncio.coroutine def _handle_message_flow(self, app_message): """ Handle protocol flow for incoming and outgoing messages, depending on service level and according to MQTT spec. paragraph 4.3-Quality of Service levels and protocol flows :param app_message: PublishMessage to handle :return: nothing. """ if app_message.qos == QOS_0: yield from self._handle_qos0_message_flow(app_message) elif app_message.qos == QOS_1: yield from self._handle_qos1_message_flow(app_message) elif app_message.qos == QOS_2: yield from self._handle_qos2_message_flow(app_message) else: raise HBMQTTException("Unexcepted QOS value '%d" % str(app_message.qos)) @asyncio.coroutine def _handle_qos0_message_flow(self, app_message): """ Handle QOS_0 application message acknowledgment For incoming messages, this method stores the message For outgoing messages, this methods sends PUBLISH :param app_message: :return: """ assert app_message.qos == QOS_0 if app_message.direction == OUTGOING: packet = app_message.build_publish_packet() # Send PUBLISH packet yield from self._send_packet(packet) app_message.publish_packet = packet elif app_message.direction == INCOMING: if app_message.publish_packet.dup_flag: self.logger.warning("[MQTT-3.3.1-2] DUP flag must set to 0 for QOS 0 message. Message ignored: %s" % repr(app_message.publish_packet)) else: try: self.session.delivered_message_queue.put_nowait(app_message) except: self.logger.warning("delivered messages queue full. QOS_0 message discarded") @asyncio.coroutine def _handle_qos1_message_flow(self, app_message): """ Handle QOS_1 application message acknowledgment For incoming messages, this method stores the message and reply with PUBACK For outgoing messages, this methods sends PUBLISH and waits for the corresponding PUBACK :param app_message: :return: """ assert app_message.qos == QOS_1 if app_message.puback_packet: raise HBMQTTException("Message '%d' has already been acknowledged" % app_message.packet_id) if app_message.direction == OUTGOING: if app_message.packet_id not in self.session.inflight_out: # Store message in session self.session.inflight_out[app_message.packet_id] = app_message if app_message.publish_packet is not None: # A Publish packet has already been sent, this is a retry publish_packet = app_message.build_publish_packet(dup=True) else: publish_packet = app_message.build_publish_packet() # Send PUBLISH packet yield from self._send_packet(publish_packet) app_message.publish_packet = publish_packet # Wait for puback waiter = asyncio.Future(loop=self._loop) self._puback_waiters[app_message.packet_id] = waiter yield from waiter del self._puback_waiters[app_message.packet_id] app_message.puback_packet = waiter.result() # Discard inflight message del self.session.inflight_out[app_message.packet_id] elif app_message.direction == INCOMING: # Initiate delivery self.logger.debug("Add message to delivery") yield from self.session.delivered_message_queue.put(app_message) # Send PUBACK puback = PubackPacket.build(app_message.packet_id) yield from self._send_packet(puback) app_message.puback_packet = puback @asyncio.coroutine def _handle_qos2_message_flow(self, app_message): """ Handle QOS_2 application message acknowledgment For incoming messages, this method stores the message, sends PUBREC, waits for PUBREL, initiate delivery and send PUBCOMP For outgoing messages, this methods sends PUBLISH, waits for PUBREC, discards messages and wait for PUBCOMP :param app_message: :return: """ assert app_message.qos == QOS_2 if app_message.direction == OUTGOING: if app_message.pubrel_packet and app_message.pubcomp_packet: raise HBMQTTException("Message '%d' has already been acknowledged" % app_message.packet_id) if not app_message.pubrel_packet: # Store message if app_message.publish_packet is not None: # This is a retry flow, no need to store just check the message exists in session if app_message.packet_id not in self.session.inflight_out: raise HBMQTTException("Unknown inflight message '%d' in session" % app_message.packet_id) publish_packet = app_message.build_publish_packet(dup=True) else: # Store message in session self.session.inflight_out[app_message.packet_id] = app_message publish_packet = app_message.build_publish_packet() # Send PUBLISH packet yield from self._send_packet(publish_packet) app_message.publish_packet = publish_packet # Wait PUBREC if app_message.packet_id in self._pubrec_waiters: # PUBREC waiter already exists for this packet ID message = "Can't add PUBREC waiter, a waiter already exists for message Id '%s'" \ % app_message.packet_id self.logger.warning(message) raise HBMQTTException(message) waiter = asyncio.Future(loop=self._loop) self._pubrec_waiters[app_message.packet_id] = waiter yield from waiter del self._pubrec_waiters[app_message.packet_id] app_message.pubrec_packet = waiter.result() if not app_message.pubcomp_packet: # Send pubrel app_message.pubrel_packet = PubrelPacket.build(app_message.packet_id) yield from self._send_packet(app_message.pubrel_packet) # Wait for PUBCOMP waiter = asyncio.Future(loop=self._loop) self._pubcomp_waiters[app_message.packet_id] = waiter yield from waiter del self._pubcomp_waiters[app_message.packet_id] app_message.pubcomp_packet = waiter.result() # Discard inflight message del self.session.inflight_out[app_message.packet_id] elif app_message.direction == INCOMING: self.session.inflight_in[app_message.packet_id] = app_message # Send pubrec pubrec_packet = PubrecPacket.build(app_message.packet_id) yield from self._send_packet(pubrec_packet) app_message.pubrec_packet = pubrec_packet # Wait PUBREL if app_message.packet_id in self._pubrel_waiters and not self._pubrel_waiters[app_message.packet_id].done(): # PUBREL waiter already exists for this packet ID message = "A waiter already exists for message Id '%s', canceling it" \ % app_message.packet_id self.logger.warning(message) self._pubrel_waiters[app_message.packet_id].cancel() try: waiter = asyncio.Future(loop=self._loop) self._pubrel_waiters[app_message.packet_id] = waiter yield from waiter del self._pubrel_waiters[app_message.packet_id] app_message.pubrel_packet = waiter.result() # Initiate delivery and discard message yield from self.session.delivered_message_queue.put(app_message) del self.session.inflight_in[app_message.packet_id] # Send pubcomp pubcomp_packet = PubcompPacket.build(app_message.packet_id) yield from self._send_packet(pubcomp_packet) app_message.pubcomp_packet = pubcomp_packet except asyncio.CancelledError: self.logger.debug("Message flow cancelled") @asyncio.coroutine def _reader_loop(self): self.logger.debug("%s Starting reader coro" % self.session.client_id) running_tasks = collections.deque() keepalive_timeout = self.session.keep_alive if keepalive_timeout <= 0: keepalive_timeout = None while True: try: self._reader_ready.set() while running_tasks and running_tasks[0].done(): running_tasks.popleft() if len(running_tasks) > 1: self.logger.debug("handler running tasks: %d" % len(running_tasks)) fixed_header = yield from asyncio.wait_for(MQTTFixedHeader.from_stream(self.reader), keepalive_timeout, loop=self._loop) if fixed_header: if fixed_header.packet_type == RESERVED_0 or fixed_header.packet_type == RESERVED_15: self.logger.warning("%s Received reserved packet, which is forbidden: closing connection" % (self.session.client_id)) yield from self.handle_connection_closed() else: cls = packet_class(fixed_header) packet = yield from cls.from_stream(self.reader, fixed_header=fixed_header) yield from self.plugins_manager.fire_event( EVENT_MQTT_PACKET_RECEIVED, packet=packet, session=self.session) task = None if packet.fixed_header.packet_type == CONNACK: task = ensure_future(self.handle_connack(packet), loop=self._loop) elif packet.fixed_header.packet_type == SUBSCRIBE: task = ensure_future(self.handle_subscribe(packet), loop=self._loop) elif packet.fixed_header.packet_type == UNSUBSCRIBE: task = ensure_future(self.handle_unsubscribe(packet), loop=self._loop) elif packet.fixed_header.packet_type == SUBACK: task = ensure_future(self.handle_suback(packet), loop=self._loop) elif packet.fixed_header.packet_type == UNSUBACK: task = ensure_future(self.handle_unsuback(packet), loop=self._loop) elif packet.fixed_header.packet_type == PUBACK: task = ensure_future(self.handle_puback(packet), loop=self._loop) elif packet.fixed_header.packet_type == PUBREC: task = ensure_future(self.handle_pubrec(packet), loop=self._loop) elif packet.fixed_header.packet_type == PUBREL: task = ensure_future(self.handle_pubrel(packet), loop=self._loop) elif packet.fixed_header.packet_type == PUBCOMP: task = ensure_future(self.handle_pubcomp(packet), loop=self._loop) elif packet.fixed_header.packet_type == PINGREQ: task = ensure_future(self.handle_pingreq(packet), loop=self._loop) elif packet.fixed_header.packet_type == PINGRESP: task = ensure_future(self.handle_pingresp(packet), loop=self._loop) elif packet.fixed_header.packet_type == PUBLISH: task = ensure_future(self.handle_publish(packet), loop=self._loop) elif packet.fixed_header.packet_type == DISCONNECT: task = ensure_future(self.handle_disconnect(packet), loop=self._loop) elif packet.fixed_header.packet_type == CONNECT: self.handle_connect(packet) else: self.logger.warning("%s Unhandled packet type: %s" % (self.session.client_id, packet.fixed_header.packet_type)) if task: running_tasks.append(task) else: self.logger.debug("%s No more data (EOF received), stopping reader coro" % self.session.client_id) break except MQTTException: self.logger.debug("Message discarded") except asyncio.CancelledError: self.logger.debug("Task cancelled, reader loop ending") break except asyncio.TimeoutError: self.logger.debug("%s Input stream read timeout" % self.session.client_id) self.handle_read_timeout() except NoDataException: self.logger.debug("%s No data available" % self.session.client_id) except BaseException as e: self.logger.warning("%s Unhandled exception in reader coro: %r" % (type(self).__name__, e)) break while running_tasks: running_tasks.popleft().cancel() yield from self.handle_connection_closed() self._reader_stopped.set() self.logger.debug("%s Reader coro stopped" % self.session.client_id) yield from self.stop() @asyncio.coroutine def _send_packet(self, packet): try: yield from packet.to_stream(self.writer) if self._keepalive_task: self._keepalive_task.cancel() self._keepalive_task = self._loop.call_later(self.keepalive_timeout, self.handle_write_timeout) yield from self.plugins_manager.fire_event(EVENT_MQTT_PACKET_SENT, packet=packet, session=self.session) except ConnectionResetError as cre: yield from self.handle_connection_closed() raise except BaseException as e: self.logger.warning("Unhandled exception: %s" % e) raise @asyncio.coroutine def mqtt_deliver_next_message(self): if self.logger.isEnabledFor(logging.DEBUG): self.logger.debug("%d message(s) available for delivery" % self.session.delivered_message_queue.qsize()) try: message = yield from self.session.delivered_message_queue.get() except asyncio.CancelledError: message = None if self.logger.isEnabledFor(logging.DEBUG): self.logger.debug("Delivering message %s" % message) return message def handle_write_timeout(self): self.logger.debug('%s write timeout unhandled' % self.session.client_id) def handle_read_timeout(self): self.logger.debug('%s read timeout unhandled' % self.session.client_id) @asyncio.coroutine def handle_connack(self, connack: ConnackPacket): self.logger.debug('%s CONNACK unhandled' % self.session.client_id) @asyncio.coroutine def handle_connect(self, connect: ConnectPacket): self.logger.debug('%s CONNECT unhandled' % self.session.client_id) @asyncio.coroutine def handle_subscribe(self, subscribe: SubscribePacket): self.logger.debug('%s SUBSCRIBE unhandled' % self.session.client_id) @asyncio.coroutine def handle_unsubscribe(self, subscribe: UnsubscribePacket): self.logger.debug('%s UNSUBSCRIBE unhandled' % self.session.client_id) @asyncio.coroutine def handle_suback(self, suback: SubackPacket): self.logger.debug('%s SUBACK unhandled' % self.session.client_id) @asyncio.coroutine def handle_unsuback(self, unsuback: UnsubackPacket): self.logger.debug('%s UNSUBACK unhandled' % self.session.client_id) @asyncio.coroutine def handle_pingresp(self, pingresp: PingRespPacket): self.logger.debug('%s PINGRESP unhandled' % self.session.client_id) @asyncio.coroutine def handle_pingreq(self, pingreq: PingReqPacket): self.logger.debug('%s PINGREQ unhandled' % self.session.client_id) @asyncio.coroutine def handle_disconnect(self, disconnect: DisconnectPacket): self.logger.debug('%s DISCONNECT unhandled' % self.session.client_id) @asyncio.coroutine def handle_connection_closed(self): self.logger.debug('%s Connection closed unhandled' % self.session.client_id) @asyncio.coroutine def handle_puback(self, puback: PubackPacket): packet_id = puback.variable_header.packet_id try: waiter = self._puback_waiters[packet_id] waiter.set_result(puback) except KeyError: self.logger.warning("Received PUBACK for unknown pending message Id: '%d'" % packet_id) except InvalidStateError: self.logger.warning("PUBACK waiter with Id '%d' already done" % packet_id) @asyncio.coroutine def handle_pubrec(self, pubrec: PubrecPacket): packet_id = pubrec.packet_id try: waiter = self._pubrec_waiters[packet_id] waiter.set_result(pubrec) except KeyError: self.logger.warning("Received PUBREC for unknown pending message with Id: %d" % packet_id) except InvalidStateError: self.logger.warning("PUBREC waiter with Id '%d' already done" % packet_id) @asyncio.coroutine def handle_pubcomp(self, pubcomp: PubcompPacket): packet_id = pubcomp.packet_id try: waiter = self._pubcomp_waiters[packet_id] waiter.set_result(pubcomp) except KeyError: self.logger.warning("Received PUBCOMP for unknown pending message with Id: %d" % packet_id) except InvalidStateError: self.logger.warning("PUBCOMP waiter with Id '%d' already done" % packet_id) @asyncio.coroutine def handle_pubrel(self, pubrel: PubrelPacket): packet_id = pubrel.packet_id try: waiter = self._pubrel_waiters[packet_id] waiter.set_result(pubrel) except KeyError: self.logger.warning("Received PUBREL for unknown pending message with Id: %d" % packet_id) except InvalidStateError: self.logger.warning("PUBREL waiter with Id '%d' already done" % packet_id) @asyncio.coroutine def handle_publish(self, publish_packet: PublishPacket): packet_id = publish_packet.variable_header.packet_id qos = publish_packet.qos incoming_message = IncomingApplicationMessage(packet_id, publish_packet.topic_name, qos, publish_packet.data, publish_packet.retain_flag) incoming_message.publish_packet = publish_packet yield from self._handle_message_flow(incoming_message) self.logger.debug("Message queue size: %d" % self.session.delivered_message_queue.qsize()) hbmqtt-0.9/hbmqtt/mqtt/puback.py0000644000076500000240000000223513114237777017055 0ustar nicostaff00000000000000# Copyright (c) 2015 Nicolas JOUANIN # # See the file license.txt for copying permission. from hbmqtt.mqtt.packet import MQTTPacket, MQTTFixedHeader, PUBACK, PacketIdVariableHeader from hbmqtt.errors import HBMQTTException class PubackPacket(MQTTPacket): VARIABLE_HEADER = PacketIdVariableHeader PAYLOAD = None @property def packet_id(self): return self.variable_header.packet_id @packet_id.setter def packet_id(self, val: int): self.variable_header.packet_id = val def __init__(self, fixed: MQTTFixedHeader=None, variable_header: PacketIdVariableHeader=None): if fixed is None: header = MQTTFixedHeader(PUBACK, 0x00) else: if fixed.packet_type is not PUBACK: raise HBMQTTException("Invalid fixed packet type %s for PubackPacket init" % fixed.packet_type) header = fixed super().__init__(header) self.variable_header = variable_header self.payload = None @classmethod def build(cls, packet_id: int): v_header = PacketIdVariableHeader(packet_id) packet = PubackPacket(variable_header=v_header) return packet hbmqtt-0.9/hbmqtt/mqtt/pubcomp.py0000644000076500000240000000224313114237777017254 0ustar nicostaff00000000000000# Copyright (c) 2015 Nicolas JOUANIN # # See the file license.txt for copying permission. from hbmqtt.mqtt.packet import MQTTPacket, MQTTFixedHeader, PUBCOMP, PacketIdVariableHeader from hbmqtt.errors import HBMQTTException class PubcompPacket(MQTTPacket): VARIABLE_HEADER = PacketIdVariableHeader PAYLOAD = None @property def packet_id(self): return self.variable_header.packet_id @packet_id.setter def packet_id(self, val: int): self.variable_header.packet_id = val def __init__(self, fixed: MQTTFixedHeader=None, variable_header: PacketIdVariableHeader=None): if fixed is None: header = MQTTFixedHeader(PUBCOMP, 0x00) else: if fixed.packet_type is not PUBCOMP: raise HBMQTTException("Invalid fixed packet type %s for PubcompPacket init" % fixed.packet_type) header = fixed super().__init__(header) self.variable_header = variable_header self.payload = None @classmethod def build(cls, packet_id: int): v_header = PacketIdVariableHeader(packet_id) packet = PubcompPacket(variable_header=v_header) return packet hbmqtt-0.9/hbmqtt/mqtt/publish.py0000644000076500000240000001161513114237777017260 0ustar nicostaff00000000000000# Copyright (c) 2015 Nicolas JOUANIN # # See the file license.txt for copying permission. from hbmqtt.mqtt.packet import MQTTPacket, MQTTFixedHeader, PUBLISH, MQTTVariableHeader, MQTTPayload from hbmqtt.errors import HBMQTTException, MQTTException from hbmqtt.codecs import * class PublishVariableHeader(MQTTVariableHeader): def __init__(self, topic_name: str, packet_id: int=None): super().__init__() if '*' in topic_name: raise MQTTException("[MQTT-3.3.2-2] Topic name in the PUBLISH Packet MUST NOT contain wildcard characters.") self.topic_name = topic_name self.packet_id = packet_id def __repr__(self): return type(self).__name__ + '(topic={0}, packet_id={1})'.format(self.topic_name, self.packet_id) def to_bytes(self): out = bytearray() out.extend(encode_string(self.topic_name)) if self.packet_id is not None: out.extend(int_to_bytes(self.packet_id, 2)) return out @classmethod @asyncio.coroutine def from_stream(cls, reader: asyncio.StreamReader, fixed_header: MQTTFixedHeader): topic_name = yield from decode_string(reader) has_qos = (fixed_header.flags >> 1) & 0x03 if has_qos: packet_id = yield from decode_packet_id(reader) else: packet_id = None return cls(topic_name, packet_id) class PublishPayload(MQTTPayload): def __init__(self, data: bytes=None): super().__init__() self.data = data def to_bytes(self, fixed_header: MQTTFixedHeader, variable_header: MQTTVariableHeader): return self.data @classmethod @asyncio.coroutine def from_stream(cls, reader: asyncio.StreamReader, fixed_header: MQTTFixedHeader, variable_header: MQTTVariableHeader): data = bytearray() data_length = fixed_header.remaining_length-variable_header.bytes_length length_read = 0 while length_read < data_length: buffer = yield from reader.read(data_length - length_read) data.extend(buffer) length_read = len(data) return cls(data) def __repr__(self): return type(self).__name__ + '(data={0!r})'.format(repr(self.data)) class PublishPacket(MQTTPacket): VARIABLE_HEADER = PublishVariableHeader PAYLOAD = PublishPayload DUP_FLAG = 0x08 RETAIN_FLAG = 0x01 QOS_FLAG = 0x06 def __init__(self, fixed: MQTTFixedHeader=None, variable_header: PublishVariableHeader=None, payload=None): if fixed is None: header = MQTTFixedHeader(PUBLISH, 0x00) else: if fixed.packet_type is not PUBLISH: raise HBMQTTException("Invalid fixed packet type %s for PublishPacket init" % fixed.packet_type) header = fixed super().__init__(header) self.variable_header = variable_header self.payload = payload def set_flags(self, dup_flag=False, qos=0, retain_flag=False): self.dup_flag = dup_flag self.retain_flag = retain_flag self.qos = qos def _set_header_flag(self, val, mask): if val: self.fixed_header.flags |= mask else: self.fixed_header.flags &= ~mask def _get_header_flag(self, mask): if self.fixed_header.flags & mask: return True else: return False @property def dup_flag(self) -> bool: return self._get_header_flag(self.DUP_FLAG) @dup_flag.setter def dup_flag(self, val: bool): self._set_header_flag(val, self.DUP_FLAG) @property def retain_flag(self) -> bool: return self._get_header_flag(self.RETAIN_FLAG) @retain_flag.setter def retain_flag(self, val: bool): self._set_header_flag(val, self.RETAIN_FLAG) @property def qos(self): return (self.fixed_header.flags & self.QOS_FLAG) >> 1 @qos.setter def qos(self, val: int): self.fixed_header.flags &= 0xf9 self.fixed_header.flags |= (val << 1) @property def packet_id(self): return self.variable_header.packet_id @packet_id.setter def packet_id(self, val: int): self.variable_header.packet_id = val @property def data(self): return self.payload.data @data.setter def data(self, data: bytes): self.payload.data = data @property def topic_name(self): return self.variable_header.topic_name @topic_name.setter def topic_name(self, name: str): self.variable_header.topic_name = name @classmethod def build(cls, topic_name: str, message: bytes, packet_id: int, dup_flag, qos, retain): v_header = PublishVariableHeader(topic_name, packet_id) payload = PublishPayload(message) packet = PublishPacket(variable_header=v_header, payload=payload) packet.dup_flag = dup_flag packet.retain_flag = retain packet.qos = qos return packet hbmqtt-0.9/hbmqtt/mqtt/pubrec.py0000644000076500000240000000223513114237777017070 0ustar nicostaff00000000000000# Copyright (c) 2015 Nicolas JOUANIN # # See the file license.txt for copying permission. from hbmqtt.mqtt.packet import MQTTPacket, MQTTFixedHeader, PUBREC, PacketIdVariableHeader from hbmqtt.errors import HBMQTTException class PubrecPacket(MQTTPacket): VARIABLE_HEADER = PacketIdVariableHeader PAYLOAD = None @property def packet_id(self): return self.variable_header.packet_id @packet_id.setter def packet_id(self, val: int): self.variable_header.packet_id = val def __init__(self, fixed: MQTTFixedHeader=None, variable_header: PacketIdVariableHeader=None): if fixed is None: header = MQTTFixedHeader(PUBREC, 0x00) else: if fixed.packet_type is not PUBREC: raise HBMQTTException("Invalid fixed packet type %s for PubrecPacket init" % fixed.packet_type) header = fixed super().__init__(header) self.variable_header = variable_header self.payload = None @classmethod def build(cls, packet_id: int): v_header = PacketIdVariableHeader(packet_id) packet = PubrecPacket(variable_header=v_header) return packet hbmqtt-0.9/hbmqtt/mqtt/pubrel.py0000644000076500000240000000224013114237777017075 0ustar nicostaff00000000000000# Copyright (c) 2015 Nicolas JOUANIN # # See the file license.txt for copying permission. from hbmqtt.mqtt.packet import MQTTPacket, MQTTFixedHeader, PUBREL, PacketIdVariableHeader from hbmqtt.errors import HBMQTTException class PubrelPacket(MQTTPacket): VARIABLE_HEADER = PacketIdVariableHeader PAYLOAD = None @property def packet_id(self): return self.variable_header.packet_id @packet_id.setter def packet_id(self, val: int): self.variable_header.packet_id = val def __init__(self, fixed: MQTTFixedHeader=None, variable_header: PacketIdVariableHeader=None): if fixed is None: header = MQTTFixedHeader(PUBREL, 0x02) # [MQTT-3.6.1-1] else: if fixed.packet_type is not PUBREL: raise HBMQTTException("Invalid fixed packet type %s for PubrelPacket init" % fixed.packet_type) header = fixed super().__init__(header) self.variable_header = variable_header self.payload = None @classmethod def build(cls, packet_id): variable_header = PacketIdVariableHeader(packet_id) return PubrelPacket(variable_header=variable_header) hbmqtt-0.9/hbmqtt/mqtt/suback.py0000644000076500000240000000451313114237777017061 0ustar nicostaff00000000000000# Copyright (c) 2015 Nicolas JOUANIN # # See the file license.txt for copying permission. from hbmqtt.mqtt.packet import MQTTPacket, MQTTFixedHeader, SUBACK, PacketIdVariableHeader, MQTTPayload, MQTTVariableHeader from hbmqtt.errors import HBMQTTException from hbmqtt.adapters import ReaderAdapter from hbmqtt.codecs import * class SubackPayload(MQTTPayload): RETURN_CODE_00 = 0x00 RETURN_CODE_01 = 0x01 RETURN_CODE_02 = 0x02 RETURN_CODE_80 = 0x80 def __init__(self, return_codes=[]): super().__init__() self.return_codes = return_codes def __repr__(self): return type(self).__name__ + '(return_codes={0})'.format(repr(self.return_codes)) def to_bytes(self, fixed_header: MQTTFixedHeader, variable_header: MQTTVariableHeader): out = b'' for return_code in self.return_codes: out += int_to_bytes(return_code, 1) return out @classmethod @asyncio.coroutine def from_stream(cls, reader: ReaderAdapter, fixed_header: MQTTFixedHeader, variable_header: MQTTVariableHeader): return_codes = [] bytes_to_read = fixed_header.remaining_length - variable_header.bytes_length for i in range(0, bytes_to_read): try: return_code_byte = yield from read_or_raise(reader, 1) return_code = bytes_to_int(return_code_byte) return_codes.append(return_code) except NoDataException: break return cls(return_codes) class SubackPacket(MQTTPacket): VARIABLE_HEADER = PacketIdVariableHeader PAYLOAD = SubackPayload def __init__(self, fixed: MQTTFixedHeader=None, variable_header: PacketIdVariableHeader=None, payload=None): if fixed is None: header = MQTTFixedHeader(SUBACK, 0x00) else: if fixed.packet_type is not SUBACK: raise HBMQTTException("Invalid fixed packet type %s for SubackPacket init" % fixed.packet_type) header = fixed super().__init__(header) self.variable_header = variable_header self.payload = payload @classmethod def build(cls, packet_id, return_codes): variable_header = cls.VARIABLE_HEADER(packet_id) payload = cls.PAYLOAD(return_codes) return cls(variable_header=variable_header, payload=payload) hbmqtt-0.9/hbmqtt/mqtt/subscribe.py0000644000076500000240000000453013114237777017571 0ustar nicostaff00000000000000# Copyright (c) 2015 Nicolas JOUANIN # # See the file license.txt for copying permission. from hbmqtt.mqtt.packet import MQTTPacket, MQTTFixedHeader, SUBSCRIBE, PacketIdVariableHeader, MQTTPayload, MQTTVariableHeader from hbmqtt.errors import HBMQTTException, MQTTException from hbmqtt.codecs import * class SubscribePayload(MQTTPayload): def __init__(self, topics=[]): super().__init__() self.topics = topics def to_bytes(self, fixed_header: MQTTFixedHeader, variable_header: MQTTVariableHeader): out = b'' for topic in self.topics: out += encode_string(topic[0]) out += int_to_bytes(topic[1], 1) return out @classmethod @asyncio.coroutine def from_stream(cls, reader: asyncio.StreamReader, fixed_header: MQTTFixedHeader, variable_header: MQTTVariableHeader): topics = [] payload_length = fixed_header.remaining_length - variable_header.bytes_length read_bytes = 0 while read_bytes < payload_length: try: topic = yield from decode_string(reader) qos_byte = yield from read_or_raise(reader, 1) qos = bytes_to_int(qos_byte) topics.append((topic, qos)) read_bytes += 2 + len(topic.encode('utf-8')) + 1 except NoDataException as exc: break return cls(topics) def __repr__(self): return type(self).__name__ + '(topics={0!r})'.format(self.topics) class SubscribePacket(MQTTPacket): VARIABLE_HEADER = PacketIdVariableHeader PAYLOAD = SubscribePayload def __init__(self, fixed: MQTTFixedHeader=None, variable_header: PacketIdVariableHeader=None, payload=None): if fixed is None: header = MQTTFixedHeader(SUBSCRIBE, 0x02) # [MQTT-3.8.1-1] else: if fixed.packet_type is not SUBSCRIBE: raise HBMQTTException("Invalid fixed packet type %s for SubscribePacket init" % fixed.packet_type) header = fixed super().__init__(header) self.variable_header = variable_header self.payload = payload @classmethod def build(cls, topics, packet_id): v_header = PacketIdVariableHeader(packet_id) payload = SubscribePayload(topics) return SubscribePacket(variable_header=v_header, payload=payload)hbmqtt-0.9/hbmqtt/mqtt/unsuback.py0000644000076500000240000000174313114237777017426 0ustar nicostaff00000000000000# Copyright (c) 2015 Nicolas JOUANIN # # See the file license.txt for copying permission. from hbmqtt.mqtt.packet import MQTTPacket, MQTTFixedHeader, UNSUBACK, PacketIdVariableHeader from hbmqtt.errors import HBMQTTException class UnsubackPacket(MQTTPacket): VARIABLE_HEADER = PacketIdVariableHeader PAYLOAD = None def __init__(self, fixed: MQTTFixedHeader=None, variable_header: PacketIdVariableHeader=None, payload=None): if fixed is None: header = MQTTFixedHeader(UNSUBACK, 0x00) else: if fixed.packet_type is not UNSUBACK: raise HBMQTTException("Invalid fixed packet type %s for UnsubackPacket init" % fixed.packet_type) header = fixed super().__init__(header) self.variable_header = variable_header self.payload = payload @classmethod def build(cls, packet_id): variable_header = PacketIdVariableHeader(packet_id) return cls(variable_header=variable_header)hbmqtt-0.9/hbmqtt/mqtt/unsubscribe.py0000644000076500000240000000411013114237777020126 0ustar nicostaff00000000000000# Copyright (c) 2015 Nicolas JOUANIN # # See the file license.txt for copying permission. from hbmqtt.mqtt.packet import MQTTPacket, MQTTFixedHeader, UNSUBSCRIBE, PacketIdVariableHeader, MQTTPayload, MQTTVariableHeader from hbmqtt.errors import HBMQTTException from hbmqtt.codecs import * class UnubscribePayload(MQTTPayload): def __init__(self, topics=[]): super().__init__() self.topics = topics def to_bytes(self, fixed_header: MQTTFixedHeader, variable_header: MQTTVariableHeader): out = b'' for topic in self.topics: out += encode_string(topic) return out @classmethod @asyncio.coroutine def from_stream(cls, reader: asyncio.StreamReader, fixed_header: MQTTFixedHeader, variable_header: MQTTVariableHeader): topics = [] payload_length = fixed_header.remaining_length - variable_header.bytes_length read_bytes = 0 while read_bytes < payload_length: try: topic = yield from decode_string(reader) topics.append(topic) read_bytes += 2 + len(topic.encode('utf-8')) except NoDataException: break return cls(topics) class UnsubscribePacket(MQTTPacket): VARIABLE_HEADER = PacketIdVariableHeader PAYLOAD = UnubscribePayload def __init__(self, fixed: MQTTFixedHeader=None, variable_header: PacketIdVariableHeader=None, payload=None): if fixed is None: header = MQTTFixedHeader(UNSUBSCRIBE, 0x02) # [MQTT-3.10.1-1] else: if fixed.packet_type is not UNSUBSCRIBE: raise HBMQTTException("Invalid fixed packet type %s for UnsubscribePacket init" % fixed.packet_type) header = fixed super().__init__(header) self.variable_header = variable_header self.payload = payload @classmethod def build(cls, topics, packet_id): v_header = PacketIdVariableHeader(packet_id) payload = UnubscribePayload(topics) return UnsubscribePacket(variable_header=v_header, payload=payload)hbmqtt-0.9/hbmqtt/plugins/0000755000076500000240000000000013114340722015711 5ustar nicostaff00000000000000hbmqtt-0.9/hbmqtt/plugins/__init__.py0000644000076500000240000000013313114237777020036 0ustar nicostaff00000000000000# Copyright (c) 2015 Nicolas JOUANIN # # See the file license.txt for copying permission. hbmqtt-0.9/hbmqtt/plugins/authentication.py0000644000076500000240000000743213114237777021327 0ustar nicostaff00000000000000# Copyright (c) 2015 Nicolas JOUANIN # # See the file license.txt for copying permission. import logging import asyncio from passlib.apps import custom_app_context as pwd_context class BaseAuthPlugin: def __init__(self, context): self.context = context try: self.auth_config = self.context.config['auth'] except KeyError: self.context.logger.warn("'auth' section not found in context configuration") def authenticate(self, *args, **kwargs): if not self.auth_config: # auth config section not found self.context.logger.warn("'auth' section not found in context configuration") return False return True class AnonymousAuthPlugin(BaseAuthPlugin): def __init__(self, context): super().__init__(context) @asyncio.coroutine def authenticate(self, *args, **kwargs): authenticated = super().authenticate(*args, **kwargs) if authenticated: allow_anonymous = self.auth_config.get('allow-anonymous', True) # allow anonymous by default if allow_anonymous: authenticated = True self.context.logger.debug("Authentication success: config allows anonymous") else: try: session = kwargs.get('session', None) authenticated = True if session.username else False if self.context.logger.isEnabledFor(logging.DEBUG): if authenticated: self.context.logger.debug("Authentication success: session has a non empty username") else: self.context.logger.debug("Authentication failure: session has an empty username") except KeyError: self.context.logger.warn("Session informations not available") authenticated = False return authenticated class FileAuthPlugin(BaseAuthPlugin): def __init__(self, context): super().__init__(context) self._users = dict() self._read_password_file() def _read_password_file(self): password_file = self.auth_config.get('password-file', None) if password_file: try: with open(password_file) as f: self.context.logger.debug("Reading user database from %s" % password_file) for l in f: line = l.strip() if not line.startswith('#'): # Allow comments in files (username, pwd_hash) = line.split(sep=":", maxsplit=3) if username: self._users[username] = pwd_hash self.context.logger.debug("user %s , hash=%s" % (username, pwd_hash)) self.context.logger.debug("%d user(s) read from file %s" % (len(self._users), password_file)) except FileNotFoundError: self.context.logger.warn("Password file %s not found" % password_file) else: self.context.logger.debug("Configuration parameter 'password_file' not found") @asyncio.coroutine def authenticate(self, *args, **kwargs): authenticated = super().authenticate(*args, **kwargs) if authenticated: session = kwargs.get('session', None) if session.username: hash = self._users.get(session.username, None) if not hash: authenticated = False self.context.logger.debug("No hash found for user '%s'" % session.username) else: authenticated = pwd_context.verify(session.password, hash) else: return None return authenticated hbmqtt-0.9/hbmqtt/plugins/logging.py0000644000076500000240000000272113114237777017732 0ustar nicostaff00000000000000# Copyright (c) 2015 Nicolas JOUANIN # # See the file license.txt for copying permission. import logging import asyncio from functools import partial class EventLoggerPlugin: def __init__(self, context): self.context = context @asyncio.coroutine def log_event(self, *args, **kwargs): self.context.logger.info("### '%s' EVENT FIRED ###" % kwargs['event_name'].replace('old', '')) def __getattr__(self, name): if name.startswith("on_"): return partial(self.log_event, event_name=name) class PacketLoggerPlugin: def __init__(self, context): self.context = context @asyncio.coroutine def on_mqtt_packet_received(self, *args, **kwargs): packet = kwargs.get('packet') session = kwargs.get('session', None) if self.context.logger.isEnabledFor(logging.DEBUG): if session: self.context.logger.debug("%s <-in-- %s" % (session.client_id, repr(packet))) else: self.context.logger.debug("<-in-- %s" % repr(packet)) @asyncio.coroutine def on_mqtt_packet_sent(self, *args, **kwargs): packet = kwargs.get('packet') session = kwargs.get('session', None) if self.context.logger.isEnabledFor(logging.DEBUG): if session: self.context.logger.debug("%s -out-> %s" % (session.client_id, repr(packet))) else: self.context.logger.debug("-out-> %s" % repr(packet)) hbmqtt-0.9/hbmqtt/plugins/manager.py0000644000076500000240000001624313114334706017710 0ustar nicostaff00000000000000# Copyright (c) 2015 Nicolas JOUANIN # # See the file license.txt for copying permission. __all__ = ['get_plugin_manager', 'BaseContext', 'PluginManager'] import pkg_resources import logging import asyncio import copy import sys if sys.version_info < (3, 5): from asyncio import async as ensure_future else: from asyncio import ensure_future from collections import namedtuple Plugin = namedtuple('Plugin', ['name', 'ep', 'object']) plugins_manager = dict() def get_plugin_manager(namespace): global plugins_manager return plugins_manager.get(namespace, None) class BaseContext: def __init__(self): self.loop = None self.logger = None class PluginManager: """ Wraps setuptools Entry point mechanism to provide a basic plugin system. Plugins are loaded for a given namespace (group). This plugin manager uses coroutines to run plugin call asynchronously in an event queue """ def __init__(self, namespace, context, loop=None): global plugins_manager if loop is not None: self._loop = loop else: self._loop = asyncio.get_event_loop() self.logger = logging.getLogger(namespace) if context is None: self.context = BaseContext() else: self.context = context self.context.loop = self._loop self._plugins = [] self._load_plugins(namespace) self._fired_events = [] plugins_manager[namespace] = self @property def app_context(self): return self.context def _load_plugins(self, namespace): self.logger.debug("Loading plugins for namespace %s" % namespace) for ep in pkg_resources.iter_entry_points(group=namespace): plugin = self._load_plugin(ep) self._plugins.append(plugin) self.logger.debug(" Plugin %s ready" % plugin.ep.name) def _load_plugin(self, ep: pkg_resources.EntryPoint): try: self.logger.debug(" Loading plugin %s" % ep) plugin = ep.load(require=True) self.logger.debug(" Initializing plugin %s" % ep) plugin_context = copy.copy(self.app_context) plugin_context.logger = self.logger.getChild(ep.name) obj = plugin(plugin_context) return Plugin(ep.name, ep, obj) except ImportError as ie: self.logger.warn("Plugin %r import failed: %s" % (ep, ie)) except pkg_resources.UnknownExtra as ue: self.logger.warn("Plugin %r dependencies resolution failed: %s" % (ep, ue)) def get_plugin(self, name): """ Get a plugin by its name from the plugins loaded for the current namespace :param name: :return: """ for p in self._plugins: if p.name == name: return p return None @asyncio.coroutine def close(self): """ Free PluginManager resources and cancel pending event methods This method call a close() coroutine for each plugin, allowing plugins to close and free resources :return: """ yield from self.map_plugin_coro("close") for task in self._fired_events: task.cancel() @property def plugins(self): """ Get the loaded plugins list :return: """ return self._plugins def _schedule_coro(self, coro): return ensure_future(coro, loop=self._loop) @asyncio.coroutine def fire_event(self, event_name, wait=False, *args, **kwargs): """ Fire an event to plugins. PluginManager schedule @asyncio.coroutinecalls for each plugin on method called "on_" + event_name For example, on_connect will be called on event 'connect' Method calls are schedule in the asyn loop. wait parameter must be set to true to wait until all mehtods are completed. :param event_name: :param args: :param kwargs: :param wait: indicates if fire_event should wait for plugin calls completion (True), or not :return: """ tasks = [] event_method_name = "on_" + event_name for plugin in self._plugins: event_method = getattr(plugin.object, event_method_name, None) if event_method: try: task = self._schedule_coro(event_method(*args, **kwargs)) tasks.append(task) def clean_fired_events(future): try: self._fired_events.remove(task) except KeyError: pass task.add_done_callback(clean_fired_events) except AssertionError: self.logger.error("Method '%s' on plugin '%s' is not a coroutine" % (event_method_name, plugin.name)) self._fired_events.extend(tasks) if wait: if tasks: yield from asyncio.wait(tasks, loop=self._loop) @asyncio.coroutine def map(self, coro, *args, **kwargs): """ Schedule a given coroutine call for each plugin. The coro called get the Plugin instance as first argument of its method call :param coro: coro to call on each plugin :param filter_plugins: list of plugin names to filter (only plugin whose name is in filter are called). None will call all plugins. [] will call None. :param args: arguments to pass to coro :param kwargs: arguments to pass to coro :return: dict containing return from coro call for each plugin """ p_list = kwargs.pop('filter_plugins', None) if p_list is None: p_list = [p.name for p in self.plugins] tasks = [] plugins_list = [] for plugin in self._plugins: if plugin.name in p_list: coro_instance = coro(plugin, *args, **kwargs) if coro_instance: try: tasks.append(self._schedule_coro(coro_instance)) plugins_list.append(plugin) except AssertionError: self.logger.error("Method '%r' on plugin '%s' is not a coroutine" % (coro, plugin.name)) if tasks: ret_list = yield from asyncio.gather(*tasks, loop=self._loop) # Create result map plugin=>ret ret_dict = {k: v for k, v in zip(plugins_list, ret_list)} else: ret_dict = {} return ret_dict @staticmethod @asyncio.coroutine def _call_coro(plugin, coro_name, *args, **kwargs): try: coro = getattr(plugin.object, coro_name, None)(*args, **kwargs) return (yield from coro) except TypeError: # Plugin doesn't implement coro_name return None @asyncio.coroutine def map_plugin_coro(self, coro_name, *args, **kwargs): """ Call a plugin declared by plugin by its name :param coro_name: :param args: :param kwargs: :return: """ return (yield from self.map(self._call_coro, coro_name, *args, **kwargs)) hbmqtt-0.9/hbmqtt/plugins/persistence.py0000644000076500000240000000452713114237777020636 0ustar nicostaff00000000000000# Copyright (c) 2015 Nicolas JOUANIN # # See the file license.txt for copying permission. import asyncio import sqlite3 import pickle class SQLitePlugin: def __init__(self, context): self.context = context self.conn = None self.cursor = None self.db_file = None try: self.persistence_config = self.context.config['persistence'] self.init_db() except KeyError: self.context.logger.warn("'persistence' section not found in context configuration") def init_db(self): self.db_file = self.persistence_config.get('file', None) if not self.db_file: self.context.logger.warn("'file' persistence parameter not found") else: try: self.conn = sqlite3.connect(self.db_file) self.cursor = self.conn.cursor() self.context.logger.info("Database file '%s' opened" % self.db_file) except Exception as e: self.context.logger.error("Error while initializing database '%s' : %s" % (self.db_file, e)) if self.cursor: self.cursor.execute("CREATE TABLE IF NOT EXISTS session(client_id TEXT PRIMARY KEY, data BLOB)") @asyncio.coroutine def save_session(self, session): if self.cursor: dump = pickle.dumps(session) try: self.cursor.execute( "INSERT OR REPLACE INTO session (client_id, data) VALUES (?,?)", (session.client_id, dump)) self.conn.commit() except Exception as e: self.context.logger.error("Failed saving session '%s': %s" % (session, e)) @asyncio.coroutine def find_session(self, client_id): if self.cursor: row = self.cursor.execute("SELECT data FROM session where client_id=?", (client_id,)).fetchone() if row: return pickle.loads(row[0]) else: return None @asyncio.coroutine def del_session(self, client_id): if self.cursor: self.cursor.execute("DELETE FROM session where client_id=?", (client_id,)) self.conn.commit() @asyncio.coroutine def on_broker_post_shutdown(self): if self.conn: self.conn.close() self.context.logger.info("Database file '%s' closed" % self.db_file) hbmqtt-0.9/hbmqtt/plugins/sys/0000755000076500000240000000000013114340722016527 5ustar nicostaff00000000000000hbmqtt-0.9/hbmqtt/plugins/sys/__init__.py0000644000076500000240000000000013114237777020645 0ustar nicostaff00000000000000hbmqtt-0.9/hbmqtt/plugins/sys/broker.py0000644000076500000240000001740013114237777020406 0ustar nicostaff00000000000000# Copyright (c) 2015 Nicolas JOUANIN # # See the file license.txt for copying permission. from datetime import datetime from hbmqtt.mqtt.packet import PUBLISH from hbmqtt.codecs import int_to_bytes_str import asyncio import sys if sys.version_info < (3, 5): from asyncio import async as ensure_future else: from asyncio import ensure_future from collections import deque DOLLAR_SYS_ROOT = '$SYS/broker/' STAT_BYTES_SENT = 'bytes_sent' STAT_BYTES_RECEIVED = 'bytes_received' STAT_MSG_SENT = 'messages_sent' STAT_MSG_RECEIVED = 'messages_received' STAT_PUBLISH_SENT = 'publish_sent' STAT_PUBLISH_RECEIVED = 'publish_received' STAT_START_TIME = 'start_time' STAT_CLIENTS_MAXIMUM = 'clients_maximum' STAT_CLIENTS_CONNECTED = 'clients_connected' STAT_CLIENTS_DISCONNECTED = 'clients_disconnected' class BrokerSysPlugin: def __init__(self, context): self.context = context # Broker statistics initialization self._stats = dict() self._sys_handle = None def _clear_stats(self): """ Initializes broker statistics data structures """ for stat in (STAT_BYTES_RECEIVED, STAT_BYTES_SENT, STAT_MSG_RECEIVED, STAT_MSG_SENT, STAT_CLIENTS_MAXIMUM, STAT_CLIENTS_CONNECTED, STAT_CLIENTS_DISCONNECTED, STAT_PUBLISH_RECEIVED, STAT_PUBLISH_SENT): self._stats[stat] = 0 @asyncio.coroutine def _broadcast_sys_topic(self, topic_basename, data): return (yield from self.context.broadcast_message(topic_basename, data)) def schedule_broadcast_sys_topic(self, topic_basename, data): return ensure_future(self._broadcast_sys_topic(DOLLAR_SYS_ROOT + topic_basename, data), loop=self.context.loop) @asyncio.coroutine def on_broker_pre_start(self, *args, **kwargs): self._clear_stats() @asyncio.coroutine def on_broker_post_start(self, *args, **kwargs): self._stats[STAT_START_TIME] = datetime.now() from hbmqtt.version import get_version version = 'HBMQTT version ' + get_version() self.context.retain_message(DOLLAR_SYS_ROOT + 'version', version.encode()) # Start $SYS topics management try: sys_interval = int(self.context.config.get('sys_interval', 0)) if sys_interval > 0: self.context.logger.debug("Setup $SYS broadcasting every %d secondes" % sys_interval) self.sys_handle = self.context.loop.call_later(sys_interval, self.broadcast_dollar_sys_topics) else: self.context.logger.debug("$SYS disabled") except KeyError: pass # 'sys_internal' config parameter not found @asyncio.coroutine def on_broker_pre_stop(self, *args, **kwargs): # Stop $SYS topics broadcasting if self.sys_handle: self.sys_handle.cancel() def broadcast_dollar_sys_topics(self): """ Broadcast dynamic $SYS topics updates and reschedule next execution depending on 'sys_interval' config parameter. """ # Update stats uptime = datetime.now() - self._stats[STAT_START_TIME] client_connected = self._stats[STAT_CLIENTS_CONNECTED] client_disconnected = self._stats[STAT_CLIENTS_DISCONNECTED] inflight_in = 0 inflight_out = 0 messages_stored = 0 for session in self.context.sessions: inflight_in += session.inflight_in_count inflight_out += session.inflight_out_count messages_stored += session.retained_messages_count messages_stored += len(self.context.retained_messages) subscriptions_count = 0 for topic in self.context.subscriptions: subscriptions_count += len(self.context.subscriptions[topic]) # Broadcast updates tasks = deque() tasks.append(self.schedule_broadcast_sys_topic('load/bytes/received', int_to_bytes_str(self._stats[STAT_BYTES_RECEIVED]))) tasks.append(self.schedule_broadcast_sys_topic('load/bytes/sent', int_to_bytes_str(self._stats[STAT_BYTES_SENT]))) tasks.append(self.schedule_broadcast_sys_topic('messages/received', int_to_bytes_str(self._stats[STAT_MSG_RECEIVED]))) tasks.append(self.schedule_broadcast_sys_topic('messages/sent', int_to_bytes_str(self._stats[STAT_MSG_SENT]))) tasks.append(self.schedule_broadcast_sys_topic('time', str(datetime.now()).encode('utf-8'))) tasks.append(self.schedule_broadcast_sys_topic('uptime', int_to_bytes_str(int(uptime.total_seconds())))) tasks.append(self.schedule_broadcast_sys_topic('uptime/formated', str(uptime).encode('utf-8'))) tasks.append(self.schedule_broadcast_sys_topic('clients/connected', int_to_bytes_str(client_connected))) tasks.append(self.schedule_broadcast_sys_topic('clients/disconnected', int_to_bytes_str(client_disconnected))) tasks.append(self.schedule_broadcast_sys_topic('clients/maximum', int_to_bytes_str(self._stats[STAT_CLIENTS_MAXIMUM]))) tasks.append(self.schedule_broadcast_sys_topic('clients/total', int_to_bytes_str(client_connected + client_disconnected))) tasks.append(self.schedule_broadcast_sys_topic('messages/inflight', int_to_bytes_str(inflight_in + inflight_out))) tasks.append(self.schedule_broadcast_sys_topic('messages/inflight/in', int_to_bytes_str(inflight_in))) tasks.append(self.schedule_broadcast_sys_topic('messages/inflight/out', int_to_bytes_str(inflight_out))) tasks.append(self.schedule_broadcast_sys_topic('messages/inflight/stored', int_to_bytes_str(messages_stored))) tasks.append(self.schedule_broadcast_sys_topic('messages/publish/received', int_to_bytes_str(self._stats[STAT_PUBLISH_RECEIVED]))) tasks.append(self.schedule_broadcast_sys_topic('messages/publish/sent', int_to_bytes_str(self._stats[STAT_PUBLISH_SENT]))) tasks.append(self.schedule_broadcast_sys_topic('messages/retained/count', int_to_bytes_str(len(self.context.retained_messages)))) tasks.append(self.schedule_broadcast_sys_topic('messages/subscriptions/count', int_to_bytes_str(subscriptions_count))) # Wait until broadcasting tasks end while tasks and tasks[0].done(): tasks.popleft() # Reschedule sys_interval = int(self.context.config['sys_interval']) self.context.logger.debug("Broadcasting $SYS topics") self.sys_handle = self.context.loop.call_later(sys_interval, self.broadcast_dollar_sys_topics) @asyncio.coroutine def on_mqtt_packet_received(self, *args, **kwargs): packet = kwargs.get('packet') if packet: packet_size = packet.bytes_length self._stats[STAT_BYTES_RECEIVED] += packet_size self._stats[STAT_MSG_RECEIVED] += 1 if packet.fixed_header.packet_type == PUBLISH: self._stats[STAT_PUBLISH_RECEIVED] += 1 @asyncio.coroutine def on_mqtt_packet_sent(self, *args, **kwargs): packet = kwargs.get('packet') if packet: packet_size = packet.bytes_length self._stats[STAT_BYTES_SENT] += packet_size self._stats[STAT_MSG_SENT] += 1 if packet.fixed_header.packet_type == PUBLISH: self._stats[STAT_PUBLISH_SENT] += 1 @asyncio.coroutine def on_broker_client_connected(self, *args, **kwargs): self._stats[STAT_CLIENTS_CONNECTED] += 1 self._stats[STAT_CLIENTS_MAXIMUM] = max(self._stats[STAT_CLIENTS_MAXIMUM], self._stats[STAT_CLIENTS_CONNECTED]) @asyncio.coroutine def on_broker_client_disconnected(self, *args, **kwargs): self._stats[STAT_CLIENTS_CONNECTED] -= 1 self._stats[STAT_CLIENTS_DISCONNECTED] += 1 hbmqtt-0.9/hbmqtt/session.py0000644000076500000240000001565513114314435016303 0ustar nicostaff00000000000000# Copyright (c) 2015 Nicolas JOUANIN # # See the file license.txt for copying permission. import asyncio from transitions import Machine from asyncio import Queue from collections import OrderedDict from hbmqtt.mqtt.publish import PublishPacket from hbmqtt.errors import HBMQTTException OUTGOING = 0 INCOMING = 1 class ApplicationMessage: """ ApplicationMessage and subclasses are used to store published message information flow. These objects can contain different information depending on the way they were created (incoming or outgoing) and the quality of service used between peers. """ def __init__(self, packet_id, topic, qos, data, retain): self.packet_id = packet_id """ Publish message `packet identifier `_""" self.topic = topic """ Publish message topic""" self.qos = qos """ Publish message Quality of Service""" self.data = data """ Publish message payload data""" self.retain = retain """ Publish message retain flag""" self.publish_packet = None """ :class:`hbmqtt.mqtt.publish.PublishPacket` instance corresponding to the `PUBLISH `_ packet in the messages flow. ``None`` if the PUBLISH packet has not already been received or sent.""" self.puback_packet = None """ :class:`hbmqtt.mqtt.puback.PubackPacket` instance corresponding to the `PUBACK `_ packet in the messages flow. ``None`` if QoS != QOS_1 or if the PUBACK packet has not already been received or sent.""" self.pubrec_packet = None """ :class:`hbmqtt.mqtt.puback.PubrecPacket` instance corresponding to the `PUBREC `_ packet in the messages flow. ``None`` if QoS != QOS_2 or if the PUBREC packet has not already been received or sent.""" self.pubrel_packet = None """ :class:`hbmqtt.mqtt.puback.PubrelPacket` instance corresponding to the `PUBREL `_ packet in the messages flow. ``None`` if QoS != QOS_2 or if the PUBREL packet has not already been received or sent.""" self.pubcomp_packet = None """ :class:`hbmqtt.mqtt.puback.PubrelPacket` instance corresponding to the `PUBCOMP `_ packet in the messages flow. ``None`` if QoS != QOS_2 or if the PUBCOMP packet has not already been received or sent.""" def build_publish_packet(self, dup=False): """ Build :class:`hbmqtt.mqtt.publish.PublishPacket` from attributes :param dup: force dup flag :return: :class:`hbmqtt.mqtt.publish.PublishPacket` built from ApplicationMessage instance attributes """ return PublishPacket.build(self.topic, self.data, self.packet_id, dup, self.qos, self.retain) def __eq__(self, other): return self.packet_id == other.packet_id class IncomingApplicationMessage(ApplicationMessage): """ Incoming :class:`~hbmqtt.session.ApplicationMessage`. """ def __init__(self, packet_id, topic, qos, data, retain): super().__init__(packet_id, topic, qos, data, retain) self.direction = INCOMING class OutgoingApplicationMessage(ApplicationMessage): """ Outgoing :class:`~hbmqtt.session.ApplicationMessage`. """ def __init__(self, packet_id, topic, qos, data, retain): super().__init__(packet_id, topic, qos, data, retain) self.direction = OUTGOING class Session: states = ['new', 'connected', 'disconnected'] def __init__(self, loop=None): self._init_states() self.remote_address = None self.remote_port = None self.client_id = None self.clean_session = None self.will_flag = False self.will_message = None self.will_qos = None self.will_retain = None self.will_topic = None self.keep_alive = 0 self.publish_retry_delay = 0 self.broker_uri = None self.username = None self.password = None self.cafile = None self.capath = None self.cadata = None self._packet_id = 0 self.parent = 0 if loop is not None: self._loop = loop else: self._loop = asyncio.get_event_loop() # Used to store outgoing ApplicationMessage while publish protocol flows self.inflight_out = OrderedDict() # Used to store incoming ApplicationMessage while publish protocol flows self.inflight_in = OrderedDict() # Stores messages retained for this session self.retained_messages = Queue(loop=self._loop) # Stores PUBLISH messages ID received in order and ready for application process self.delivered_message_queue = Queue(loop=self._loop) def _init_states(self): self.transitions = Machine(states=Session.states, initial='new') self.transitions.add_transition(trigger='connect', source='new', dest='connected') self.transitions.add_transition(trigger='connect', source='disconnected', dest='connected') self.transitions.add_transition(trigger='disconnect', source='connected', dest='disconnected') self.transitions.add_transition(trigger='disconnect', source='new', dest='disconnected') self.transitions.add_transition(trigger='disconnect', source='disconnected', dest='disconnected') @property def next_packet_id(self): self._packet_id += 1 if self._packet_id > 65535: self._packet_id = 1 while self._packet_id in self.inflight_in or self._packet_id in self.inflight_out: self._packet_id += 1 if self._packet_id > 65535: raise HBMQTTException("More than 65525 messages pending. No free packet ID") return self._packet_id @property def inflight_in_count(self): return len(self.inflight_in) @property def inflight_out_count(self): return len(self.inflight_out) @property def retained_messages_count(self): return self.retained_messages.qsize() def __repr__(self): return type(self).__name__ + '(clientId={0}, state={1})'.format(self.client_id, self.transitions.state) def __getstate__(self): state = self.__dict__.copy() # Remove the unpicklable entries. # del state['transitions'] del state['retained_messages'] del state['delivered_message_queue'] return state def __setstate(self, state): self.__dict__.update(state) self.retained_messages = Queue() self.delivered_message_queue = Queue() def __eq__(self, other): return self.client_id == other.client_idhbmqtt-0.9/hbmqtt/utils.py0000644000076500000240000000225513114237777015765 0ustar nicostaff00000000000000# Copyright (c) 2015 Nicolas JOUANIN # # See the file license.txt for copying permission. import yaml def not_in_dict_or_none(dict, key): """ Check if a key exists in a map and if it's not None :param dict: map to look for key :param key: key to find :return: true if key is in dict and not None """ if key not in dict or dict[key] is None: return True else: return False def format_client_message(session=None, address=None, port=None): if session: return "(client id=%s)" % session.client_id elif address is not None and port is not None: return "(client @=%s:%d)" % (address, port) else: return "(unknown client)" def gen_client_id(): """ Generates random client ID :return: """ import random gen_id = 'hbmqtt/' for i in range(7, 23): gen_id += chr(random.randint(0, 74) + 48) return gen_id def read_yaml_config(config_file): config = None try: with open(config_file, 'r') as stream: config = yaml.load(stream) except yaml.YAMLError as exc: logger.error("Invalid config_file %s: %s" % (config_file, exc)) return confighbmqtt-0.9/hbmqtt/version.py0000644000076500000240000000354213114237777016312 0ustar nicostaff00000000000000# Copyright (c) 2015 Nicolas JOUANIN # # See the file license.txt for copying permission. import datetime import os import subprocess # Version Management from https://gist.github.com/gilsondev/2790884 def get_version(version=None): "Returns a PEP 386-compliant version number from VERSION." if version is None: from hbmqtt import VERSION as version else: assert len(version) == 5 assert version[3] in ('alpha', 'beta', 'rc', 'final') # Now build the two parts of the version number: # main = X.Y[.Z] # sub = .devN - for pre-alpha releases # | {a|b|c}N - for alpha, beta and rc releases parts = 2 if version[2] == 0 else 3 main = '.'.join(str(x) for x in version[:parts]) sub = '' if version[3] == 'alpha' and version[4] == 0: git_changeset = get_git_changeset() if git_changeset: sub = '.dev%s' % git_changeset elif version[3] != 'final': mapping = {'alpha': 'a', 'beta': 'b', 'rc': 'c'} sub = mapping[version[3]] + str(version[4]) return str(main + sub) def get_git_changeset(): """Returns a numeric identifier of the latest git changeset. The result is the UTC timestamp of the changeset in YYYYMMDDHHMMSS format. This value isn't guaranteed to be unique, but collisions are very unlikely, so it's sufficient for generating the development version numbers. """ repo_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) git_log = subprocess.Popen('git log --pretty=format:%ct --quiet -1 HEAD', stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True, cwd=repo_dir, universal_newlines=True) timestamp = git_log.communicate()[0] try: timestamp = datetime.datetime.utcfromtimestamp(int(timestamp)) except ValueError: return None return timestamp.strftime('%Y%m%d%H%M%S')hbmqtt-0.9/hbmqtt.egg-info/0000755000076500000240000000000013114340722015722 5ustar nicostaff00000000000000hbmqtt-0.9/hbmqtt.egg-info/dependency_links.txt0000644000076500000240000000000113114340722021770 0ustar nicostaff00000000000000 hbmqtt-0.9/hbmqtt.egg-info/entry_points.txt0000644000076500000240000000125613114340722021224 0ustar nicostaff00000000000000[console_scripts] hbmqtt = scripts.broker_script:main hbmqtt_pub = scripts.pub_script:main hbmqtt_sub = scripts.sub_script:main [hbmqtt.broker.plugins] auth_anonymous = hbmqtt.plugins.authentication:AnonymousAuthPlugin auth_file = hbmqtt.plugins.authentication:FileAuthPlugin broker_sys = hbmqtt.plugins.sys.broker:BrokerSysPlugin packet_logger_plugin = hbmqtt.plugins.logging:PacketLoggerPlugin [hbmqtt.client.plugins] packet_logger_plugin = hbmqtt.plugins.logging:PacketLoggerPlugin [hbmqtt.test.plugins] event_plugin = tests.plugins.test_manager:EventTestPlugin packet_logger_plugin = hbmqtt.plugins.logging:PacketLoggerPlugin test_plugin = tests.plugins.test_manager:TestPlugin hbmqtt-0.9/hbmqtt.egg-info/PKG-INFO0000644000076500000240000000135613114340722017024 0ustar nicostaff00000000000000Metadata-Version: 1.1 Name: hbmqtt Version: 0.9 Summary: MQTT client/brocker using Python 3.4 asyncio library Home-page: https://github.com/beerfactory/hbmqtt Author: Nicolas Jouanin Author-email: nico@beerfactory.org License: MIT Description: UNKNOWN Platform: all Classifier: Development Status :: 3 - Alpha Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: MIT License Classifier: Operating System :: POSIX Classifier: Operating System :: MacOS Classifier: Operating System :: Microsoft :: Windows Classifier: Programming Language :: Python :: 3.4 Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: 3.6 Classifier: Topic :: Communications Classifier: Topic :: Internet hbmqtt-0.9/hbmqtt.egg-info/requires.txt0000644000076500000240000000006413114340722020322 0ustar nicostaff00000000000000transitions==0.2.5 websockets passlib docopt pyyaml hbmqtt-0.9/hbmqtt.egg-info/SOURCES.txt0000644000076500000240000000337513114340722017616 0ustar nicostaff00000000000000MANIFEST.in license.txt setup.cfg setup.py hbmqtt/__init__.py hbmqtt/adapters.py hbmqtt/broker.py hbmqtt/client.py hbmqtt/codecs.py hbmqtt/errors.py hbmqtt/session.py hbmqtt/utils.py hbmqtt/version.py hbmqtt.egg-info/PKG-INFO hbmqtt.egg-info/SOURCES.txt hbmqtt.egg-info/dependency_links.txt hbmqtt.egg-info/entry_points.txt hbmqtt.egg-info/requires.txt hbmqtt.egg-info/top_level.txt hbmqtt/mqtt/__init__.py hbmqtt/mqtt/connack.py hbmqtt/mqtt/connect.py hbmqtt/mqtt/constants.py hbmqtt/mqtt/disconnect.py hbmqtt/mqtt/packet.py hbmqtt/mqtt/pingreq.py hbmqtt/mqtt/pingresp.py hbmqtt/mqtt/puback.py hbmqtt/mqtt/pubcomp.py hbmqtt/mqtt/publish.py hbmqtt/mqtt/pubrec.py hbmqtt/mqtt/pubrel.py hbmqtt/mqtt/suback.py hbmqtt/mqtt/subscribe.py hbmqtt/mqtt/unsuback.py hbmqtt/mqtt/unsubscribe.py hbmqtt/mqtt/protocol/__init__.py hbmqtt/mqtt/protocol/broker_handler.py hbmqtt/mqtt/protocol/client_handler.py hbmqtt/mqtt/protocol/handler.py hbmqtt/plugins/__init__.py hbmqtt/plugins/authentication.py hbmqtt/plugins/logging.py hbmqtt/plugins/manager.py hbmqtt/plugins/persistence.py hbmqtt/plugins/sys/__init__.py hbmqtt/plugins/sys/broker.py scripts/__init__.py scripts/broker_script.py scripts/default_broker.yaml scripts/default_client.yaml scripts/pub_script.py scripts/sub_script.py tests/mqtt/__init__.py tests/mqtt/test_connect.py tests/mqtt/test_packet.py tests/mqtt/test_puback.py tests/mqtt/test_pubcomp.py tests/mqtt/test_publish.py tests/mqtt/test_pubrec.py tests/mqtt/test_pubrel.py tests/mqtt/test_suback.py tests/mqtt/test_subscribe.py tests/mqtt/test_unsuback.py tests/mqtt/test_unsubscribe.py tests/mqtt/protocol/__init__.py tests/mqtt/protocol/test_handler.py tests/plugins/__init__.py tests/plugins/test_authentication.py tests/plugins/test_manager.py tests/plugins/test_persistence.pyhbmqtt-0.9/hbmqtt.egg-info/top_level.txt0000644000076500000240000000002513114340722020451 0ustar nicostaff00000000000000hbmqtt scripts tests hbmqtt-0.9/license.txt0000644000076500000240000000207213114237777015134 0ustar nicostaff00000000000000The MIT License (MIT) Copyright (c) 2015 Nicolas JOUANIN Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. hbmqtt-0.9/MANIFEST.in0000644000076500000240000000006513114334706014475 0ustar nicostaff00000000000000recursive-include scripts *.yaml include license.txt hbmqtt-0.9/PKG-INFO0000644000076500000240000000135613114340722014033 0ustar nicostaff00000000000000Metadata-Version: 1.1 Name: hbmqtt Version: 0.9 Summary: MQTT client/brocker using Python 3.4 asyncio library Home-page: https://github.com/beerfactory/hbmqtt Author: Nicolas Jouanin Author-email: nico@beerfactory.org License: MIT Description: UNKNOWN Platform: all Classifier: Development Status :: 3 - Alpha Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: MIT License Classifier: Operating System :: POSIX Classifier: Operating System :: MacOS Classifier: Operating System :: Microsoft :: Windows Classifier: Programming Language :: Python :: 3.4 Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: 3.6 Classifier: Topic :: Communications Classifier: Topic :: Internet hbmqtt-0.9/scripts/0000755000076500000240000000000013114340722014420 5ustar nicostaff00000000000000hbmqtt-0.9/scripts/__init__.py0000644000076500000240000000000013114237777016536 0ustar nicostaff00000000000000hbmqtt-0.9/scripts/broker_script.py0000644000076500000240000000374213114237777017667 0ustar nicostaff00000000000000# Copyright (c) 2015 Nicolas JOUANIN # # See the file license.txt for copying permission. """ HBMQTT - MQTT 3.1.1 broker Usage: hbmqtt --version hbmqtt (-h | --help) hbmqtt [-c ] [-d] Options: -h --help Show this screen. --version Show version. -c Broker configuration file (YAML format) -d Enable debug messages """ import sys import logging import asyncio import os from hbmqtt.broker import Broker from hbmqtt.version import get_version from docopt import docopt from hbmqtt.utils import read_yaml_config default_config = { 'listeners': { 'default': { 'type': 'tcp', 'bind': '0.0.0.0:1883', }, }, 'sys_interval': 10, 'auth': { 'allow-anonymous': True, 'password-file': os.path.join(os.path.dirname(os.path.realpath(__file__)), "passwd"), 'plugins': [ 'auth_file', 'auth_anonymous' ] } } logger = logging.getLogger(__name__) def main(*args, **kwargs): if sys.version_info[:2] < (3, 4): logger.fatal("Error: Python 3.4+ is required") sys.exit(-1) arguments = docopt(__doc__, version=get_version()) formatter = "[%(asctime)s] :: %(levelname)s - %(message)s" if arguments['-d']: level = logging.DEBUG else: level = logging.INFO logging.basicConfig(level=level, format=formatter) config = None if arguments['-c']: config = read_yaml_config(arguments['-c']) else: config = read_yaml_config(os.path.join(os.path.dirname(os.path.realpath(__file__)), 'default_broker.yaml')) logger.debug("Using default configuration") loop = asyncio.get_event_loop() broker = Broker(config) try: loop.run_until_complete(broker.start()) loop.run_forever() except KeyboardInterrupt: loop.run_until_complete(broker.shutdown()) finally: loop.close() if __name__ == "__main__": main()hbmqtt-0.9/scripts/default_broker.yaml0000644000076500000240000000022313114237777020310 0ustar nicostaff00000000000000listeners: default: type: tcp bind: 0.0.0.0:1883 sys_interval: 20 auth: allow-anonymous: true plugins: - auth_file - auth_anonymoushbmqtt-0.9/scripts/default_client.yaml0000644000076500000240000000021013114237777020276 0ustar nicostaff00000000000000keep_alive: 10 ping_delay: 1 default_qos: 0 default_retain: false auto_reconnect: false reconnect_max_interval: 10 reconnect_retries: 2 hbmqtt-0.9/scripts/pub_script.py0000644000076500000240000001334613114237777017172 0ustar nicostaff00000000000000# Copyright (c) 2015 Nicolas JOUANIN # # See the file license.txt for copying permission. """ hbmqtt_pub - MQTT 3.1.1 publisher Usage: hbmqtt_pub --version hbmqtt_pub (-h | --help) hbmqtt_pub --url BROKER_URL -t TOPIC (-f FILE | -l | -m MESSAGE | -n | -s) [-c CONFIG_FILE] [-i CLIENT_ID] [-q | --qos QOS] [-d] [-k KEEP_ALIVE] [--clean-session] [--ca-file CAFILE] [--ca-path CAPATH] [--ca-data CADATA] [ --will-topic WILL_TOPIC [--will-message WILL_MESSAGE] [--will-qos WILL_QOS] [--will-retain] ] [-r] Options: -h --help Show this screen. --version Show version. --url BROKER_URL Broker connection URL (musr conform to MQTT URI scheme (see https://github.com/mqtt/mqtt.github.io/wiki/URI-Scheme>) -c CONFIG_FILE Broker configuration file (YAML format) -i CLIENT_ID Id to use as client ID. -q | --qos QOS Quality of service to use for the message, from 0, 1 and 2. Defaults to 0. -r Set retain flag on connect -t TOPIC Message topic -m MESSAGE Message data to send -f FILE Read file by line and publish message for each line -s Read from stdin and publish message for each line -k KEEP_ALIVE Keep alive timeout in second --clean-session Clean session on connect (defaults to False) --ca-file CAFILE] CA file --ca-path CAPATH] CA Path --ca-data CADATA CA data --will-topic WILL_TOPIC --will-message WILL_MESSAGE --will-qos WILL_QOS --will-retain -d Enable debug messages """ import sys import logging import asyncio import os from hbmqtt.client import MQTTClient, ConnectException from hbmqtt.version import get_version from docopt import docopt from hbmqtt.utils import read_yaml_config if sys.version_info < (3, 5): from asyncio import async as ensure_future else: from asyncio import ensure_future logger = logging.getLogger(__name__) def _gen_client_id(): import os import socket pid = os.getpid() hostname = socket.gethostname() return "hbmqtt_pub/%d-%s" % (pid, hostname) def _get_qos(arguments): try: return int(arguments['--qos'][0]) except: return None def _get_message(arguments): if arguments['-n']: yield b'' if arguments['-m']: yield arguments['-m'].encode(encoding='utf-8') if arguments['-f']: try: with open(arguments['-f'], 'r') as f: for line in f: yield line.encode(encoding='utf-8') except: logger.error("Failed to read file '%s'" % arguments['-f']) if arguments['-l']: import sys for line in sys.stdin: if line: yield line.encode(encoding='utf-8') if arguments['-s']: import sys message = bytearray() for line in sys.stdin: message.extend(line.encode(encoding='utf-8')) yield message @asyncio.coroutine def do_pub(client, arguments): running_tasks = [] try: logger.info("%s Connecting to broker" % client.client_id) yield from client.connect(uri=arguments['--url'], cleansession=arguments['--clean-session'], cafile=arguments['--ca-file'], capath=arguments['--ca-path'], cadata=arguments['--ca-data']) qos = _get_qos(arguments) topic = arguments['-t'] retain = arguments['-r'] for message in _get_message(arguments): logger.info("%s Publishing to '%s'" % (client.client_id, topic)) task = ensure_future(client.publish(topic, message, qos, retain)) running_tasks.append(task) if running_tasks: yield from asyncio.wait(running_tasks) yield from client.disconnect() logger.info("%s Disconnected from broker" % client.client_id) except KeyboardInterrupt: yield from client.disconnect() logger.info("%s Disconnected from broker" % client.client_id) except ConnectException as ce: logger.fatal("connection to '%s' failed: %r" % (arguments['--url'], ce)) except asyncio.CancelledError as cae: logger.fatal("Publish canceled due to prvious error") def main(*args, **kwargs): if sys.version_info[:2] < (3, 4): logger.fatal("Error: Python 3.4+ is required") sys.exit(-1) arguments = docopt(__doc__, version=get_version()) #print(arguments) formatter = "[%(asctime)s] :: %(levelname)s - %(message)s" if arguments['-d']: level = logging.DEBUG else: level = logging.INFO logging.basicConfig(level=level, format=formatter) config = None if arguments['-c']: config = read_yaml_config(arguments['-c']) else: config = read_yaml_config(os.path.join(os.path.dirname(os.path.realpath(__file__)), 'default_client.yaml')) logger.debug("Using default configuration") loop = asyncio.get_event_loop() client_id = arguments.get("-i", None) if not client_id: client_id = _gen_client_id() if arguments['-k']: config['keep_alive'] = int(arguments['-k']) if arguments['--will-topic'] and arguments['--will-message'] and arguments['--will-qos']: config['will'] = dict() config['will']['topic'] = arguments['--will-topic'] config['will']['message'] = arguments['--will-message'].encode('utf-8') config['will']['qos'] = int(arguments['--will-qos']) config['will']['retain'] = arguments['--will-retain'] client = MQTTClient(client_id=client_id, config=config, loop=loop) loop.run_until_complete(do_pub(client, arguments)) loop.close() if __name__ == "__main__": main()hbmqtt-0.9/scripts/sub_script.py0000644000076500000240000001166213114237777017174 0ustar nicostaff00000000000000# Copyright (c) 2015 Nicolas JOUANIN # # See the file license.txt for copying permission. """ hbmqtt_sub - MQTT 3.1.1 publisher Usage: hbmqtt_sub --version hbmqtt_sub (-h | --help) hbmqtt_sub --url BROKER_URL -t TOPIC... [-n COUNT] [-c CONFIG_FILE] [-i CLIENT_ID] [-q | --qos QOS] [-d] [-k KEEP_ALIVE] [--clean-session] [--ca-file CAFILE] [--ca-path CAPATH] [--ca-data CADATA] [ --will-topic WILL_TOPIC [--will-message WILL_MESSAGE] [--will-qos WILL_QOS] [--will-retain] ] Options: -h --help Show this screen. --version Show version. --url BROKER_URL Broker connection URL (musr conform to MQTT URI scheme (see https://github.com/mqtt/mqtt.github.io/wiki/URI-Scheme>) -c CONFIG_FILE Broker configuration file (YAML format) -i CLIENT_ID Id to use as client ID. -n COUNT Number of messages to read before ending. -q | --qos QOS Quality of service desired to receive messages, from 0, 1 and 2. Defaults to 0. -t TOPIC... Topic filter to subcribe -k KEEP_ALIVE Keep alive timeout in second --clean-session Clean session on connect (defaults to False) --ca-file CAFILE] CA file --ca-path CAPATH] CA Path --ca-data CADATA CA data --will-topic WILL_TOPIC --will-message WILL_MESSAGE --will-qos WILL_QOS --will-retain -d Enable debug messages """ import sys import logging import asyncio import os from hbmqtt.client import MQTTClient, ConnectException from hbmqtt.errors import MQTTException from hbmqtt.version import get_version from docopt import docopt from hbmqtt.mqtt.constants import QOS_0 from hbmqtt.utils import read_yaml_config logger = logging.getLogger(__name__) def _gen_client_id(): import os import socket pid = os.getpid() hostname = socket.gethostname() return "hbmqtt_sub/%d-%s" % (pid, hostname) def _get_qos(arguments): try: return int(arguments['--qos'][0]) except: return QOS_0 @asyncio.coroutine def do_sub(client, arguments): try: yield from client.connect(uri=arguments['--url'], cleansession=arguments['--clean-session'], cafile=arguments['--ca-file'], capath=arguments['--ca-path'], cadata=arguments['--ca-data']) qos = _get_qos(arguments) filters=[] for topic in arguments['-t']: filters.append((topic, qos)) yield from client.subscribe(filters) if arguments['-n']: max_count = int(arguments['-n']) else: max_count = None count = 0 while True: if max_count and count >= max_count: break try: message = yield from client.deliver_message() count += 1 sys.stdout.buffer.write(message.publish_packet.data) sys.stdout.write('\n') except MQTTException: logger.debug("Error reading packet") yield from client.disconnect() except KeyboardInterrupt: yield from client.disconnect() except ConnectException as ce: logger.fatal("connection to '%s' failed: %r" % (arguments['--url'], ce)) except asyncio.CancelledError as cae: logger.fatal("Publish canceled due to prvious error") def main(*args, **kwargs): if sys.version_info[:2] < (3, 4): logger.fatal("Error: Python 3.4+ is required") sys.exit(-1) arguments = docopt(__doc__, version=get_version()) #print(arguments) formatter = "[%(asctime)s] :: %(levelname)s - %(message)s" if arguments['-d']: level = logging.DEBUG else: level = logging.INFO logging.basicConfig(level=level, format=formatter) config = None if arguments['-c']: config = read_yaml_config(arguments['-c']) else: config = read_yaml_config(os.path.join(os.path.dirname(os.path.realpath(__file__)), 'default_client.yaml')) logger.debug(os.path.join(os.path.dirname(os.path.realpath(__file__)), 'default_client.yaml')) logger.debug("Using default configuration") loop = asyncio.get_event_loop() client_id = arguments.get("-i", None) if not client_id: client_id = _gen_client_id() if arguments['-k']: config['keep_alive'] = int(arguments['-k']) if arguments['--will-topic'] and arguments['--will-message'] and arguments['--will-qos']: config['will'] = dict() config['will']['topic'] = arguments['--will-topic'] config['will']['message'] = arguments['--will-message'].encode('utf-8') config['will']['qos'] = int(arguments['--will-qos']) config['will']['retain'] = arguments['--will-retain'] client = MQTTClient(client_id=client_id, config=config, loop=loop) loop.run_until_complete(do_sub(client, arguments)) loop.close() if __name__ == "__main__": main()hbmqtt-0.9/setup.cfg0000644000076500000240000000014113114340722014546 0ustar nicostaff00000000000000[bdist_wheel] python-tag = py34.py35 [egg_info] tag_svn_revision = 0 tag_date = 0 tag_build = hbmqtt-0.9/setup.py0000644000076500000240000000424513114337427014460 0ustar nicostaff00000000000000# Copyright (c) 2015 Nicolas JOUANIN # # See the file license.txt for copying permission. from setuptools import setup, find_packages from hbmqtt.version import get_version setup( name="hbmqtt", version=get_version(), description="MQTT client/brocker using Python 3.4 asyncio library", author="Nicolas Jouanin", author_email='nico@beerfactory.org', url="https://github.com/beerfactory/hbmqtt", license='MIT', packages=find_packages(exclude=['tests']), include_package_data=True, platforms='all', install_requires=[ 'transitions==0.2.5', 'websockets', 'passlib', 'docopt', 'pyyaml' ], classifiers=[ 'Development Status :: 3 - Alpha', 'Intended Audience :: Developers', 'License :: OSI Approved :: MIT License', 'Operating System :: POSIX', 'Operating System :: MacOS', 'Operating System :: Microsoft :: Windows', 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', 'Topic :: Communications', 'Topic :: Internet' ], entry_points = { 'hbmqtt.test.plugins': [ 'test_plugin = tests.plugins.test_manager:TestPlugin', 'event_plugin = tests.plugins.test_manager:EventTestPlugin', 'packet_logger_plugin = hbmqtt.plugins.logging:PacketLoggerPlugin', ], 'hbmqtt.broker.plugins': [ # 'event_logger_plugin = hbmqtt.plugins.logging:EventLoggerPlugin', 'packet_logger_plugin = hbmqtt.plugins.logging:PacketLoggerPlugin', 'auth_anonymous = hbmqtt.plugins.authentication:AnonymousAuthPlugin', 'auth_file = hbmqtt.plugins.authentication:FileAuthPlugin', 'broker_sys = hbmqtt.plugins.sys.broker:BrokerSysPlugin', ], 'hbmqtt.client.plugins': [ 'packet_logger_plugin = hbmqtt.plugins.logging:PacketLoggerPlugin', ], 'console_scripts': [ 'hbmqtt = scripts.broker_script:main', 'hbmqtt_pub = scripts.pub_script:main', 'hbmqtt_sub = scripts.sub_script:main', ] } ) hbmqtt-0.9/tests/0000755000076500000240000000000013114340722014073 5ustar nicostaff00000000000000hbmqtt-0.9/tests/mqtt/0000755000076500000240000000000013114340722015060 5ustar nicostaff00000000000000hbmqtt-0.9/tests/mqtt/__init__.py0000644000076500000240000000002413114237777017204 0ustar nicostaff00000000000000__author__ = 'nico' hbmqtt-0.9/tests/mqtt/protocol/0000755000076500000240000000000013114340722016721 5ustar nicostaff00000000000000hbmqtt-0.9/tests/mqtt/protocol/__init__.py0000644000076500000240000000002413114237777021045 0ustar nicostaff00000000000000__author__ = 'nico' hbmqtt-0.9/tests/mqtt/protocol/test_handler.py0000644000076500000240000005160213114237777021772 0ustar nicostaff00000000000000# Copyright (c) 2015 Nicolas JOUANIN # # See the file license.txt for copying permission. import unittest import asyncio import logging import random from hbmqtt.plugins.manager import PluginManager from hbmqtt.session import Session, OutgoingApplicationMessage, IncomingApplicationMessage from hbmqtt.mqtt.protocol.handler import ProtocolHandler from hbmqtt.adapters import StreamWriterAdapter, StreamReaderAdapter from hbmqtt.mqtt.constants import * from hbmqtt.mqtt.publish import PublishPacket from hbmqtt.mqtt.puback import PubackPacket from hbmqtt.mqtt.pubrec import PubrecPacket from hbmqtt.mqtt.pubrel import PubrelPacket from hbmqtt.mqtt.pubcomp import PubcompPacket formatter = "[%(asctime)s] %(name)s {%(filename)s:%(lineno)d} %(levelname)s - %(message)s" logging.basicConfig(level=logging.DEBUG, format=formatter) log = logging.getLogger(__name__) def rand_packet_id(): return random.randint(0, 65535) def adapt(reader, writer): return StreamReaderAdapter(reader), StreamWriterAdapter(writer) class ProtocolHandlerTest(unittest.TestCase): def setUp(self): self.loop = asyncio.new_event_loop() asyncio.set_event_loop(self.loop) self.plugin_manager = PluginManager("hbmqtt.test.plugins", context=None, loop=self.loop) def tearDown(self): self.loop.close() def test_init_handler(self): s = Session() handler = ProtocolHandler(self.plugin_manager, loop=self.loop) self.assertIsNone(handler.session) self.assertIs(handler._loop, self.loop) self.check_empty_waiters(handler) def test_start_stop(self): @asyncio.coroutine def server_mock(reader, writer): pass @asyncio.coroutine def test_coro(): try: s = Session() reader, writer = yield from asyncio.open_connection('127.0.0.1', 8888) reader_adapted, writer_adapted = adapt(reader, writer) handler = ProtocolHandler(self.plugin_manager) handler.attach(s, reader_adapted, writer_adapted) yield from self.start_handler(handler, s) yield from self.stop_handler(handler, s) future.set_result(True) except Exception as ae: future.set_exception(ae) future = asyncio.Future(loop=self.loop) coro = asyncio.start_server(server_mock, '127.0.0.1', 8888) server = self.loop.run_until_complete(coro) self.loop.run_until_complete(test_coro()) server.close() self.loop.run_until_complete(server.wait_closed()) if future.exception(): raise future.exception() def test_publish_qos0(self): @asyncio.coroutine def server_mock(reader, writer): try: packet = yield from PublishPacket.from_stream(reader) self.assertEquals(packet.variable_header.topic_name, '/topic') self.assertEquals(packet.qos, QOS_0) self.assertIsNone(packet.packet_id) except Exception as ae: future.set_exception(ae) @asyncio.coroutine def test_coro(): try: s = Session() reader, writer = yield from asyncio.open_connection('127.0.0.1', 8888, loop=self.loop) reader_adapted, writer_adapted = adapt(reader, writer) handler = ProtocolHandler(self.plugin_manager, loop=self.loop) handler.attach(s, reader_adapted, writer_adapted) yield from self.start_handler(handler, s) message = yield from handler.mqtt_publish('/topic', b'test_data', QOS_0, False) self.assertIsInstance(message, OutgoingApplicationMessage) self.assertIsNotNone(message.publish_packet) self.assertIsNone(message.puback_packet) self.assertIsNone(message.pubrec_packet) self.assertIsNone(message.pubrel_packet) self.assertIsNone(message.pubcomp_packet) yield from self.stop_handler(handler, s) future.set_result(True) except Exception as ae: future.set_exception(ae) future = asyncio.Future(loop=self.loop) coro = asyncio.start_server(server_mock, '127.0.0.1', 8888, loop=self.loop) server = self.loop.run_until_complete(coro) self.loop.run_until_complete(test_coro()) server.close() self.loop.run_until_complete(server.wait_closed()) if future.exception(): raise future.exception() def test_publish_qos1(self): @asyncio.coroutine def server_mock(reader, writer): packet = yield from PublishPacket.from_stream(reader) try: self.assertEquals(packet.variable_header.topic_name, '/topic') self.assertEquals(packet.qos, QOS_1) self.assertIsNotNone(packet.packet_id) self.assertIn(packet.packet_id, self.session.inflight_out) self.assertIn(packet.packet_id, self.handler._puback_waiters) puback = PubackPacket.build(packet.packet_id) yield from puback.to_stream(writer) except Exception as ae: future.set_exception(ae) @asyncio.coroutine def test_coro(): try: reader, writer = yield from asyncio.open_connection('127.0.0.1', 8888, loop=self.loop) reader_adapted, writer_adapted = adapt(reader, writer) self.handler = ProtocolHandler(self.plugin_manager, loop=self.loop) self.handler.attach(self.session, reader_adapted, writer_adapted) yield from self.start_handler(self.handler, self.session) message = yield from self.handler.mqtt_publish('/topic', b'test_data', QOS_1, False) self.assertIsInstance(message, OutgoingApplicationMessage) self.assertIsNotNone(message.publish_packet) self.assertIsNotNone(message.puback_packet) self.assertIsNone(message.pubrec_packet) self.assertIsNone(message.pubrel_packet) self.assertIsNone(message.pubcomp_packet) yield from self.stop_handler(self.handler, self.session) if not future.done(): future.set_result(True) except Exception as ae: future.set_exception(ae) self.handler = None self.session = Session() future = asyncio.Future(loop=self.loop) coro = asyncio.start_server(server_mock, '127.0.0.1', 8888, loop=self.loop) server = self.loop.run_until_complete(coro) self.loop.run_until_complete(test_coro()) server.close() self.loop.run_until_complete(server.wait_closed()) if future.exception(): raise future.exception() def test_publish_qos2(self): @asyncio.coroutine def server_mock(reader, writer): try: packet = yield from PublishPacket.from_stream(reader) self.assertEquals(packet.topic_name, '/topic') self.assertEquals(packet.qos, QOS_2) self.assertIsNotNone(packet.packet_id) self.assertIn(packet.packet_id, self.session.inflight_out) self.assertIn(packet.packet_id, self.handler._pubrec_waiters) pubrec = PubrecPacket.build(packet.packet_id) yield from pubrec.to_stream(writer) pubrel = yield from PubrelPacket.from_stream(reader) self.assertIn(packet.packet_id, self.handler._pubcomp_waiters) pubcomp = PubcompPacket.build(packet.packet_id) yield from pubcomp.to_stream(writer) except Exception as ae: future.set_exception(ae) @asyncio.coroutine def test_coro(): try: reader, writer = yield from asyncio.open_connection('127.0.0.1', 8888, loop=self.loop) reader_adapted, writer_adapted = adapt(reader, writer) self.handler = ProtocolHandler(self.plugin_manager, loop=self.loop) self.handler.attach(self.session, reader_adapted, writer_adapted) yield from self.start_handler(self.handler, self.session) message = yield from self.handler.mqtt_publish('/topic', b'test_data', QOS_2, False) self.assertIsInstance(message, OutgoingApplicationMessage) self.assertIsNotNone(message.publish_packet) self.assertIsNone(message.puback_packet) self.assertIsNotNone(message.pubrec_packet) self.assertIsNotNone(message.pubrel_packet) self.assertIsNotNone(message.pubcomp_packet) yield from self.stop_handler(self.handler, self.session) if not future.done(): future.set_result(True) except Exception as ae: future.set_exception(ae) self.handler = None self.session = Session() future = asyncio.Future(loop=self.loop) coro = asyncio.start_server(server_mock, '127.0.0.1', 8888, loop=self.loop) server = self.loop.run_until_complete(coro) self.loop.run_until_complete(test_coro()) server.close() self.loop.run_until_complete(server.wait_closed()) if future.exception(): raise future.exception() def test_receive_qos0(self): @asyncio.coroutine def server_mock(reader, writer): packet = PublishPacket.build('/topic', b'test_data', rand_packet_id(), False, QOS_0, False) yield from packet.to_stream(writer) @asyncio.coroutine def test_coro(): try: reader, writer = yield from asyncio.open_connection('127.0.0.1', 8888, loop=self.loop) reader_adapted, writer_adapted = adapt(reader, writer) self.handler = ProtocolHandler(self.plugin_manager, loop=self.loop) self.handler.attach(self.session, reader_adapted, writer_adapted) yield from self.start_handler(self.handler, self.session) message = yield from self.handler.mqtt_deliver_next_message() self.assertIsInstance(message, IncomingApplicationMessage) self.assertIsNotNone(message.publish_packet) self.assertIsNone(message.puback_packet) self.assertIsNone(message.pubrec_packet) self.assertIsNone(message.pubrel_packet) self.assertIsNone(message.pubcomp_packet) yield from self.stop_handler(self.handler, self.session) future.set_result(True) except Exception as ae: future.set_exception(ae) self.handler = None self.session = Session() future = asyncio.Future(loop=self.loop) coro = asyncio.start_server(server_mock, '127.0.0.1', 8888, loop=self.loop) server = self.loop.run_until_complete(coro) self.loop.run_until_complete(test_coro()) server.close() self.loop.run_until_complete(server.wait_closed()) if future.exception(): raise future.exception() def test_receive_qos1(self): @asyncio.coroutine def server_mock(reader, writer): try: packet = PublishPacket.build('/topic', b'test_data', rand_packet_id(), False, QOS_1, False) yield from packet.to_stream(writer) puback = yield from PubackPacket.from_stream(reader) self.assertIsNotNone(puback) self.assertEqual(packet.packet_id, puback.packet_id) except Exception as ae: print(ae) future.set_exception(ae) @asyncio.coroutine def test_coro(): try: reader, writer = yield from asyncio.open_connection('127.0.0.1', 8888, loop=self.loop) reader_adapted, writer_adapted = adapt(reader, writer) self.handler = ProtocolHandler(self.plugin_manager, loop=self.loop) self.handler.attach(self.session, reader_adapted, writer_adapted) yield from self.start_handler(self.handler, self.session) message = yield from self.handler.mqtt_deliver_next_message() self.assertIsInstance(message, IncomingApplicationMessage) self.assertIsNotNone(message.publish_packet) self.assertIsNotNone(message.puback_packet) self.assertIsNone(message.pubrec_packet) self.assertIsNone(message.pubrel_packet) self.assertIsNone(message.pubcomp_packet) yield from self.stop_handler(self.handler, self.session) future.set_result(True) except Exception as ae: future.set_exception(ae) self.handler = None self.session = Session() future = asyncio.Future(loop=self.loop) self.event = asyncio.Event(loop=self.loop) coro = asyncio.start_server(server_mock, '127.0.0.1', 8888, loop=self.loop) server = self.loop.run_until_complete(coro) self.loop.run_until_complete(test_coro()) server.close() self.loop.run_until_complete(server.wait_closed()) if future.exception(): raise future.exception() def test_receive_qos2(self): @asyncio.coroutine def server_mock(reader, writer): try: packet = PublishPacket.build('/topic', b'test_data', rand_packet_id(), False, QOS_2, False) yield from packet.to_stream(writer) pubrec = yield from PubrecPacket.from_stream(reader) self.assertIsNotNone(pubrec) self.assertEqual(packet.packet_id, pubrec.packet_id) self.assertIn(packet.packet_id, self.handler._pubrel_waiters) pubrel = PubrelPacket.build(packet.packet_id) yield from pubrel.to_stream(writer) pubcomp = yield from PubcompPacket.from_stream(reader) self.assertIsNotNone(pubcomp) self.assertEqual(packet.packet_id, pubcomp.packet_id) except Exception as ae: future.set_exception(ae) @asyncio.coroutine def test_coro(): try: reader, writer = yield from asyncio.open_connection('127.0.0.1', 8888, loop=self.loop) reader_adapted, writer_adapted = adapt(reader, writer) self.handler = ProtocolHandler(self.plugin_manager, loop=self.loop) self.handler.attach(self.session, reader_adapted, writer_adapted) yield from self.start_handler(self.handler, self.session) message = yield from self.handler.mqtt_deliver_next_message() self.assertIsInstance(message, IncomingApplicationMessage) self.assertIsNotNone(message.publish_packet) self.assertIsNone(message.puback_packet) self.assertIsNotNone(message.pubrec_packet) self.assertIsNotNone(message.pubrel_packet) self.assertIsNotNone(message.pubcomp_packet) yield from self.stop_handler(self.handler, self.session) future.set_result(True) except Exception as ae: future.set_exception(ae) self.handler = None self.session = Session() future = asyncio.Future(loop=self.loop) coro = asyncio.start_server(server_mock, '127.0.0.1', 8888, loop=self.loop) server = self.loop.run_until_complete(coro) self.loop.run_until_complete(test_coro()) server.close() self.loop.run_until_complete(server.wait_closed()) if future.exception(): raise future.exception() @asyncio.coroutine def start_handler(self, handler, session): self.check_empty_waiters(handler) self.check_no_message(session) yield from handler.start() self.assertTrue(handler._reader_ready) @asyncio.coroutine def stop_handler(self, handler, session): yield from handler.stop() self.assertTrue(handler._reader_stopped) self.check_empty_waiters(handler) self.check_no_message(session) def check_empty_waiters(self, handler): self.assertFalse(handler._puback_waiters) self.assertFalse(handler._pubrec_waiters) self.assertFalse(handler._pubrel_waiters) self.assertFalse(handler._pubcomp_waiters) def check_no_message(self, session): self.assertFalse(session.inflight_out) self.assertFalse(session.inflight_in) def test_publish_qos1_retry(self): @asyncio.coroutine def server_mock(reader, writer): packet = yield from PublishPacket.from_stream(reader) try: self.assertEquals(packet.topic_name, '/topic') self.assertEquals(packet.qos, QOS_1) self.assertIsNotNone(packet.packet_id) self.assertIn(packet.packet_id, self.session.inflight_out) self.assertIn(packet.packet_id, self.handler._puback_waiters) puback = PubackPacket.build(packet.packet_id) yield from puback.to_stream(writer) except Exception as ae: future.set_exception(ae) @asyncio.coroutine def test_coro(): try: reader, writer = yield from asyncio.open_connection('127.0.0.1', 8888, loop=self.loop) reader_adapted, writer_adapted = adapt(reader, writer) self.handler = ProtocolHandler(self.plugin_manager, loop=self.loop) self.handler.attach(self.session, reader_adapted, writer_adapted) yield from self.handler.start() yield from self.stop_handler(self.handler, self.session) if not future.done(): future.set_result(True) except Exception as ae: future.set_exception(ae) self.handler = None self.session = Session() message = OutgoingApplicationMessage(1, '/topic', QOS_1, b'test_data', False) message.publish_packet = PublishPacket.build('/topic', b'test_data', rand_packet_id(), False, QOS_1, False) self.session.inflight_out[1] = message future = asyncio.Future(loop=self.loop) coro = asyncio.start_server(server_mock, '127.0.0.1', 8888, loop=self.loop) server = self.loop.run_until_complete(coro) self.loop.run_until_complete(test_coro()) server.close() self.loop.run_until_complete(server.wait_closed()) if future.exception(): raise future.exception() def test_publish_qos2_retry(self): @asyncio.coroutine def server_mock(reader, writer): try: packet = yield from PublishPacket.from_stream(reader) self.assertEquals(packet.topic_name, '/topic') self.assertEquals(packet.qos, QOS_2) self.assertIsNotNone(packet.packet_id) self.assertIn(packet.packet_id, self.session.inflight_out) self.assertIn(packet.packet_id, self.handler._pubrec_waiters) pubrec = PubrecPacket.build(packet.packet_id) yield from pubrec.to_stream(writer) pubrel = yield from PubrelPacket.from_stream(reader) self.assertIn(packet.packet_id, self.handler._pubcomp_waiters) pubcomp = PubcompPacket.build(packet.packet_id) yield from pubcomp.to_stream(writer) except Exception as ae: future.set_exception(ae) @asyncio.coroutine def test_coro(): try: reader, writer = yield from asyncio.open_connection('127.0.0.1', 8888, loop=self.loop) reader_adapted, writer_adapted = adapt(reader, writer) self.handler = ProtocolHandler(self.plugin_manager, loop=self.loop) self.handler.attach(self.session, reader_adapted, writer_adapted) yield from self.handler.start() yield from self.stop_handler(self.handler, self.session) if not future.done(): future.set_result(True) except Exception as ae: future.set_exception(ae) self.handler = None self.session = Session() message = OutgoingApplicationMessage(1, '/topic', QOS_2, b'test_data', False) message.publish_packet = PublishPacket.build('/topic', b'test_data', rand_packet_id(), False, QOS_2, False) self.session.inflight_out[1] = message future = asyncio.Future(loop=self.loop) coro = asyncio.start_server(server_mock, '127.0.0.1', 8888, loop=self.loop) server = self.loop.run_until_complete(coro) self.loop.run_until_complete(test_coro()) server.close() self.loop.run_until_complete(server.wait_closed()) if future.exception(): raise future.exception() hbmqtt-0.9/tests/mqtt/test_connect.py0000644000076500000240000001516713114237777020153 0ustar nicostaff00000000000000# Copyright (c) 2015 Nicolas JOUANIN # # See the file license.txt for copying permission. import unittest import asyncio from hbmqtt.mqtt.connect import ConnectPacket, ConnectVariableHeader, ConnectPayload from hbmqtt.mqtt.packet import MQTTFixedHeader, CONNECT from hbmqtt.adapters import BufferReader class ConnectPacketTest(unittest.TestCase): def setUp(self): self.loop = asyncio.new_event_loop() def test_decode_ok(self): data = b'\x10\x3e\x00\x04MQTT\x04\xce\x00\x00\x00\x0a0123456789\x00\x09WillTopic\x00\x0bWillMessage\x00\x04user\x00\x08password' stream = BufferReader(data) message = self.loop.run_until_complete(ConnectPacket.from_stream(stream)) self.assertEqual(message.variable_header.proto_name, "MQTT") self.assertEqual(message.variable_header.proto_level, 4) self.assertTrue(message.variable_header.username_flag) self.assertTrue(message.variable_header.password_flag) self.assertFalse(message.variable_header.will_retain_flag) self.assertEqual(message.variable_header.will_qos, 1) self.assertTrue(message.variable_header.will_flag) self.assertTrue(message.variable_header.clean_session_flag) self.assertFalse(message.variable_header.reserved_flag) self.assertEqual(message.payload.client_id, '0123456789') self.assertEqual(message.payload.will_topic, 'WillTopic') self.assertEqual(message.payload.will_message, b'WillMessage') self.assertEqual(message.payload.username, 'user') self.assertEqual(message.payload.password, 'password') def test_decode_ok_will_flag(self): data = b'\x10\x26\x00\x04MQTT\x04\xca\x00\x00\x00\x0a0123456789\x00\x04user\x00\x08password' stream = BufferReader(data) message = self.loop.run_until_complete(ConnectPacket.from_stream(stream)) self.assertEqual(message.variable_header.proto_name, "MQTT") self.assertEqual(message.variable_header.proto_level, 4) self.assertTrue(message.variable_header.username_flag) self.assertTrue(message.variable_header.password_flag) self.assertFalse(message.variable_header.will_retain_flag) self.assertEqual(message.variable_header.will_qos, 1) self.assertFalse(message.variable_header.will_flag) self.assertTrue(message.variable_header.clean_session_flag) self.assertFalse(message.variable_header.reserved_flag) self.assertEqual(message.payload.client_id, '0123456789') self.assertEqual(message.payload.will_topic, None) self.assertEqual(message.payload.will_message, None) self.assertEqual(message.payload.username, 'user') self.assertEqual(message.payload.password, 'password') def test_decode_fail_reserved_flag(self): data = b'\x10\x3e\x00\x04MQTT\x04\xcf\x00\x00\x00\x0a0123456789\x00\x09WillTopic\x00\x0bWillMessage\x00\x04user\x00\x08password' stream = BufferReader(data) message = self.loop.run_until_complete(ConnectPacket.from_stream(stream)) self.assertTrue(message.variable_header.reserved_flag) def test_decode_fail_miss_clientId(self): data = b'\x10\x0a\x00\x04MQTT\x04\xce\x00\x00' stream = BufferReader(data) message = self.loop.run_until_complete(ConnectPacket.from_stream(stream)) self.assertIs(message.payload.client_id, None) def test_decode_fail_miss_willtopic(self): data = b'\x10\x16\x00\x04MQTT\x04\xce\x00\x00\x00\x0a0123456789' stream = BufferReader(data) message = self.loop.run_until_complete(ConnectPacket.from_stream(stream)) self.assertIs(message.payload.will_topic, None) def test_decode_fail_miss_username(self): data = b'\x10\x2e\x00\x04MQTT\x04\xce\x00\x00\x00\x0a0123456789\x00\x09WillTopic\x00\x0bWillMessage' stream = BufferReader(data) message = self.loop.run_until_complete(ConnectPacket.from_stream(stream)) self.assertIs(message.payload.username, None) def test_decode_fail_miss_password(self): data = b'\x10\x34\x00\x04MQTT\x04\xce\x00\x00\x00\x0a0123456789\x00\x09WillTopic\x00\x0bWillMessage\x00\x04user' stream = BufferReader(data) message = self.loop.run_until_complete(ConnectPacket.from_stream(stream)) self.assertIs(message.payload.password, None) def test_encode(self): header = MQTTFixedHeader(CONNECT, 0x00, 0) variable_header = ConnectVariableHeader(0xce, 0, 'MQTT', 4) payload = ConnectPayload('0123456789', 'WillTopic', b'WillMessage', 'user', 'password') message = ConnectPacket(header, variable_header, payload) encoded = message.to_bytes() self.assertEqual(encoded, b'\x10\x3e\x00\x04MQTT\x04\xce\x00\x00\x00\x0a0123456789\x00\x09WillTopic\x00\x0bWillMessage\x00\x04user\x00\x08password') def test_getattr_ok(self): data = b'\x10\x3e\x00\x04MQTT\x04\xce\x00\x00\x00\x0a0123456789\x00\x09WillTopic\x00\x0bWillMessage\x00\x04user\x00\x08password' stream = BufferReader(data) message = self.loop.run_until_complete(ConnectPacket.from_stream(stream)) self.assertEqual(message.variable_header.proto_name, "MQTT") self.assertEqual(message.proto_name, "MQTT") self.assertEqual(message.variable_header.proto_level, 4) self.assertEqual(message.proto_level, 4) self.assertTrue(message.variable_header.username_flag) self.assertTrue(message.username_flag) self.assertTrue(message.variable_header.password_flag) self.assertTrue(message.password_flag) self.assertFalse(message.variable_header.will_retain_flag) self.assertFalse(message.will_retain_flag) self.assertEqual(message.variable_header.will_qos, 1) self.assertEqual(message.will_qos, 1) self.assertTrue(message.variable_header.will_flag) self.assertTrue(message.will_flag) self.assertTrue(message.variable_header.clean_session_flag) self.assertTrue(message.clean_session_flag) self.assertFalse(message.variable_header.reserved_flag) self.assertFalse(message.reserved_flag) self.assertEqual(message.payload.client_id, '0123456789') self.assertEqual(message.client_id, '0123456789') self.assertEqual(message.payload.will_topic, 'WillTopic') self.assertEqual(message.will_topic, 'WillTopic') self.assertEqual(message.payload.will_message, b'WillMessage') self.assertEqual(message.will_message, b'WillMessage') self.assertEqual(message.payload.username, 'user') self.assertEqual(message.username, 'user') self.assertEqual(message.payload.password, 'password') self.assertEqual(message.password, 'password') hbmqtt-0.9/tests/mqtt/test_packet.py0000644000076500000240000000351213114237777017760 0ustar nicostaff00000000000000# Copyright (c) 2015 Nicolas JOUANIN # # See the file license.txt for copying permission. import unittest import asyncio from hbmqtt.mqtt.packet import CONNECT, MQTTFixedHeader from hbmqtt.errors import MQTTException from hbmqtt.adapters import BufferReader class TestMQTTFixedHeaderTest(unittest.TestCase): def setUp(self): self.loop = asyncio.new_event_loop() def test_from_bytes(self): data = b'\x10\x7f' stream = BufferReader(data) header = self.loop.run_until_complete(MQTTFixedHeader.from_stream(stream)) self.assertEqual(header.packet_type, CONNECT) self.assertFalse(header.flags & 0x08) self.assertEqual((header.flags & 0x06) >> 1, 0) self.assertFalse(header.flags & 0x01) self.assertEqual(header.remaining_length, 127) def test_from_bytes_with_length(self): data = b'\x10\xff\xff\xff\x7f' stream = BufferReader(data) header = self.loop.run_until_complete(MQTTFixedHeader.from_stream(stream)) self.assertEqual(header.packet_type, CONNECT) self.assertFalse(header.flags & 0x08) self.assertEqual((header.flags & 0x06) >> 1, 0) self.assertFalse(header.flags & 0x01) self.assertEqual(header.remaining_length, 268435455) def test_from_bytes_ko_with_length(self): data = b'\x10\xff\xff\xff\xff\x7f' stream = BufferReader(data) with self.assertRaises(MQTTException): self.loop.run_until_complete(MQTTFixedHeader.from_stream(stream)) def test_to_bytes(self): header = MQTTFixedHeader(CONNECT, 0x00, 0) data = header.to_bytes() self.assertEqual(data, b'\x10\x00') def test_to_bytes_2(self): header = MQTTFixedHeader(CONNECT, 0x00, 268435455) data = header.to_bytes() self.assertEqual(data, b'\x10\xff\xff\xff\x7f') hbmqtt-0.9/tests/mqtt/test_puback.py0000644000076500000240000000150313114237777017754 0ustar nicostaff00000000000000# Copyright (c) 2015 Nicolas JOUANIN # # See the file license.txt for copying permission. import unittest from hbmqtt.mqtt.puback import PubackPacket, PacketIdVariableHeader from hbmqtt.codecs import * from hbmqtt.adapters import BufferReader class PubackPacketTest(unittest.TestCase): def setUp(self): self.loop = asyncio.new_event_loop() def test_from_stream(self): data = b'\x40\x02\x00\x0a' stream = BufferReader(data) message = self.loop.run_until_complete(PubackPacket.from_stream(stream)) self.assertEqual(message.variable_header.packet_id, 10) def test_to_bytes(self): variable_header = PacketIdVariableHeader(10) publish = PubackPacket(variable_header=variable_header) out = publish.to_bytes() self.assertEqual(out, b'\x40\x02\x00\x0a') hbmqtt-0.9/tests/mqtt/test_pubcomp.py0000644000076500000240000000151013114237777020152 0ustar nicostaff00000000000000# Copyright (c) 2015 Nicolas JOUANIN # # See the file license.txt for copying permission. import unittest from hbmqtt.mqtt.pubcomp import PubcompPacket, PacketIdVariableHeader from hbmqtt.adapters import BufferReader from hbmqtt.codecs import * class PubcompPacketTest(unittest.TestCase): def setUp(self): self.loop = asyncio.new_event_loop() def test_from_stream(self): data = b'\x70\x02\x00\x0a' stream = BufferReader(data) message = self.loop.run_until_complete(PubcompPacket.from_stream(stream)) self.assertEqual(message.variable_header.packet_id, 10) def test_to_bytes(self): variable_header = PacketIdVariableHeader(10) publish = PubcompPacket(variable_header=variable_header) out = publish.to_bytes() self.assertEqual(out, b'\x70\x02\x00\x0a') hbmqtt-0.9/tests/mqtt/test_publish.py0000644000076500000240000001224513114237777020162 0ustar nicostaff00000000000000# Copyright (c) 2015 Nicolas JOUANIN # # See the file license.txt for copying permission. import unittest from hbmqtt.mqtt.publish import PublishPacket, PublishVariableHeader, PublishPayload from hbmqtt.adapters import BufferReader from hbmqtt.codecs import * from hbmqtt.mqtt.constants import QOS_0, QOS_1, QOS_2 class PublishPacketTest(unittest.TestCase): def setUp(self): self.loop = asyncio.new_event_loop() def test_from_stream_qos_0(self): data = b'\x31\x11\x00\x05topic0123456789' stream = BufferReader(data) message = self.loop.run_until_complete(PublishPacket.from_stream(stream)) self.assertEqual(message.variable_header.topic_name, 'topic') self.assertEqual(message.variable_header.packet_id, None) self.assertFalse((message.fixed_header.flags >> 1) & 0x03) self.assertTrue(message.fixed_header.flags & 0x01) self.assertTrue(message.payload.data, b'0123456789') def test_from_stream_qos_2(self): data = b'\x37\x13\x00\x05topic\x00\x0a0123456789' stream = BufferReader(data) message = self.loop.run_until_complete(PublishPacket.from_stream(stream)) self.assertEqual(message.variable_header.topic_name, 'topic') self.assertEqual(message.variable_header.packet_id, 10) self.assertTrue((message.fixed_header.flags >> 1) & 0x03) self.assertTrue(message.fixed_header.flags & 0x01) self.assertTrue(message.payload.data, b'0123456789') def test_to_stream_no_packet_id(self): variable_header = PublishVariableHeader('topic', None) payload = PublishPayload(b'0123456789') publish = PublishPacket(variable_header=variable_header, payload=payload) out = publish.to_bytes() self.assertEqual(out, b'\x30\x11\x00\x05topic0123456789') def test_to_stream_packet(self): variable_header = PublishVariableHeader('topic', 10) payload = PublishPayload(b'0123456789') publish = PublishPacket(variable_header=variable_header, payload=payload) out = publish.to_bytes() self.assertEqual(out, b'\x30\x13\x00\x05topic\00\x0a0123456789') def test_build(self): packet = PublishPacket.build('/topic', b'data', 1, False, QOS_0, False) self.assertEquals(packet.packet_id, 1) self.assertFalse(packet.dup_flag) self.assertEquals(packet.qos, QOS_0) self.assertFalse(packet.retain_flag) packet = PublishPacket.build('/topic', b'data', 1, False, QOS_1, False) self.assertEquals(packet.packet_id, 1) self.assertFalse(packet.dup_flag) self.assertEquals(packet.qos, QOS_1) self.assertFalse(packet.retain_flag) packet = PublishPacket.build('/topic', b'data', 1, False, QOS_2, False) self.assertEquals(packet.packet_id, 1) self.assertFalse(packet.dup_flag) self.assertEquals(packet.qos, QOS_2) self.assertFalse(packet.retain_flag) packet = PublishPacket.build('/topic', b'data', 1, True, QOS_0, False) self.assertEquals(packet.packet_id, 1) self.assertTrue(packet.dup_flag) self.assertEquals(packet.qos, QOS_0) self.assertFalse(packet.retain_flag) packet = PublishPacket.build('/topic', b'data', 1, True, QOS_1, False) self.assertEquals(packet.packet_id, 1) self.assertTrue(packet.dup_flag) self.assertEquals(packet.qos, QOS_1) self.assertFalse(packet.retain_flag) packet = PublishPacket.build('/topic', b'data', 1, True, QOS_2, False) self.assertEquals(packet.packet_id, 1) self.assertTrue(packet.dup_flag) self.assertEquals(packet.qos, QOS_2) self.assertFalse(packet.retain_flag) packet = PublishPacket.build('/topic', b'data', 1, False, QOS_0, True) self.assertEquals(packet.packet_id, 1) self.assertFalse(packet.dup_flag) self.assertEquals(packet.qos, QOS_0) self.assertTrue(packet.retain_flag) packet = PublishPacket.build('/topic', b'data', 1, False, QOS_1, True) self.assertEquals(packet.packet_id, 1) self.assertFalse(packet.dup_flag) self.assertEquals(packet.qos, QOS_1) self.assertTrue(packet.retain_flag) packet = PublishPacket.build('/topic', b'data', 1, False, QOS_2, True) self.assertEquals(packet.packet_id, 1) self.assertFalse(packet.dup_flag) self.assertEquals(packet.qos, QOS_2) self.assertTrue(packet.retain_flag) packet = PublishPacket.build('/topic', b'data', 1, True, QOS_0, True) self.assertEquals(packet.packet_id, 1) self.assertTrue(packet.dup_flag) self.assertEquals(packet.qos, QOS_0) self.assertTrue(packet.retain_flag) packet = PublishPacket.build('/topic', b'data', 1, True, QOS_1, True) self.assertEquals(packet.packet_id, 1) self.assertTrue(packet.dup_flag) self.assertEquals(packet.qos, QOS_1) self.assertTrue(packet.retain_flag) packet = PublishPacket.build('/topic', b'data', 1, True, QOS_2, True) self.assertEquals(packet.packet_id, 1) self.assertTrue(packet.dup_flag) self.assertEquals(packet.qos, QOS_2) self.assertTrue(packet.retain_flag) hbmqtt-0.9/tests/mqtt/test_pubrec.py0000644000076500000240000000150213114237777017766 0ustar nicostaff00000000000000# Copyright (c) 2015 Nicolas JOUANIN # # See the file license.txt for copying permission. import unittest from hbmqtt.mqtt.pubrec import PubrecPacket, PacketIdVariableHeader from hbmqtt.adapters import BufferReader from hbmqtt.codecs import * class PubrecPacketTest(unittest.TestCase): def setUp(self): self.loop = asyncio.new_event_loop() def test_from_stream(self): data = b'\x50\x02\x00\x0a' stream = BufferReader(data) message = self.loop.run_until_complete(PubrecPacket.from_stream(stream)) self.assertEqual(message.variable_header.packet_id, 10) def test_to_bytes(self): variable_header = PacketIdVariableHeader(10) publish = PubrecPacket(variable_header=variable_header) out = publish.to_bytes() self.assertEqual(out, b'\x50\x02\x00\x0a')hbmqtt-0.9/tests/mqtt/test_pubrel.py0000644000076500000240000000150213114237777017777 0ustar nicostaff00000000000000# Copyright (c) 2015 Nicolas JOUANIN # # See the file license.txt for copying permission. import unittest from hbmqtt.mqtt.pubrel import PubrelPacket, PacketIdVariableHeader from hbmqtt.codecs import * from hbmqtt.adapters import BufferReader class PubrelPacketTest(unittest.TestCase): def setUp(self): self.loop = asyncio.new_event_loop() def test_from_stream(self): data = b'\x60\x02\x00\x0a' stream = BufferReader(data) message = self.loop.run_until_complete(PubrelPacket.from_stream(stream)) self.assertEqual(message.variable_header.packet_id, 10) def test_to_bytes(self): variable_header = PacketIdVariableHeader(10) publish = PubrelPacket(variable_header=variable_header) out = publish.to_bytes() self.assertEqual(out, b'\x62\x02\x00\x0a')hbmqtt-0.9/tests/mqtt/test_suback.py0000644000076500000240000000263413114237777017765 0ustar nicostaff00000000000000# Copyright (c) 2015 Nicolas JOUANIN # # See the file license.txt for copying permission. import unittest from hbmqtt.mqtt.suback import SubackPacket, SubackPayload from hbmqtt.mqtt.packet import PacketIdVariableHeader from hbmqtt.adapters import BufferReader from hbmqtt.codecs import * class SubackPacketTest(unittest.TestCase): def setUp(self): self.loop = asyncio.new_event_loop() def test_from_stream(self): data = b'\x90\x06\x00\x0a\x00\x01\x02\x80' stream = BufferReader(data) message = self.loop.run_until_complete(SubackPacket.from_stream(stream)) self.assertEqual(message.payload.return_codes[0], SubackPayload.RETURN_CODE_00) self.assertEqual(message.payload.return_codes[1], SubackPayload.RETURN_CODE_01) self.assertEqual(message.payload.return_codes[2], SubackPayload.RETURN_CODE_02) self.assertEqual(message.payload.return_codes[3], SubackPayload.RETURN_CODE_80) def test_to_stream(self): variable_header = PacketIdVariableHeader(10) payload = SubackPayload( [SubackPayload.RETURN_CODE_00, SubackPayload.RETURN_CODE_01, SubackPayload.RETURN_CODE_02, SubackPayload.RETURN_CODE_80 ]) suback = SubackPacket(variable_header=variable_header, payload=payload) out = suback.to_bytes() self.assertEqual(out, b'\x90\x06\x00\x0a\x00\x01\x02\x80') hbmqtt-0.9/tests/mqtt/test_subscribe.py0000644000076500000240000000245613114237777020500 0ustar nicostaff00000000000000# Copyright (c) 2015 Nicolas JOUANIN # # See the file license.txt for copying permission. import unittest from hbmqtt.mqtt.subscribe import SubscribePacket, SubscribePayload from hbmqtt.mqtt.packet import PacketIdVariableHeader from hbmqtt.mqtt.constants import * from hbmqtt.codecs import * from hbmqtt.adapters import BufferReader class SubscribePacketTest(unittest.TestCase): def setUp(self): self.loop = asyncio.new_event_loop() def test_from_stream(self): data = b'\x80\x0e\x00\x0a\x00\x03a/b\x01\x00\x03c/d\x02' stream = BufferReader(data) message = self.loop.run_until_complete(SubscribePacket.from_stream(stream)) (topic, qos) = message.payload.topics[0] self.assertEqual(topic, 'a/b') self.assertEqual(qos, QOS_1) (topic, qos) = message.payload.topics[1] self.assertEqual(topic, 'c/d') self.assertEqual(qos, QOS_2) def test_to_stream(self): variable_header = PacketIdVariableHeader(10) payload = SubscribePayload( [ ('a/b', QOS_1), ('c/d', QOS_2) ]) publish = SubscribePacket(variable_header=variable_header, payload=payload) out = publish.to_bytes() self.assertEqual(out, b'\x82\x0e\x00\x0a\x00\x03a/b\x01\x00\x03c/d\x02') hbmqtt-0.9/tests/mqtt/test_unsuback.py0000644000076500000240000000155413114237777020330 0ustar nicostaff00000000000000# Copyright (c) 2015 Nicolas JOUANIN # # See the file license.txt for copying permission. import unittest from hbmqtt.mqtt.unsuback import UnsubackPacket from hbmqtt.mqtt.packet import PacketIdVariableHeader from hbmqtt.adapters import BufferReader from hbmqtt.codecs import * class UnsubackPacketTest(unittest.TestCase): def setUp(self): self.loop = asyncio.new_event_loop() def test_from_stream(self): data = b'\xb0\x02\x00\x0a' stream = BufferReader(data) message = self.loop.run_until_complete(UnsubackPacket.from_stream(stream)) self.assertEqual(message.variable_header.packet_id, 10) def test_to_stream(self): variable_header = PacketIdVariableHeader(10) publish = UnsubackPacket(variable_header=variable_header) out = publish.to_bytes() self.assertEqual(out, b'\xb0\x02\x00\x0a') hbmqtt-0.9/tests/mqtt/test_unsubscribe.py0000644000076500000240000000206113114237777021033 0ustar nicostaff00000000000000# Copyright (c) 2015 Nicolas JOUANIN # # See the file license.txt for copying permission. import unittest from hbmqtt.mqtt.unsubscribe import UnsubscribePacket, UnubscribePayload from hbmqtt.mqtt.packet import PacketIdVariableHeader from hbmqtt.adapters import BufferReader from hbmqtt.codecs import * class UnsubscribePacketTest(unittest.TestCase): def setUp(self): self.loop = asyncio.new_event_loop() def test_from_stream(self): data = b'\xa2\x0c\x00\n\x00\x03a/b\x00\x03c/d' stream = BufferReader(data) message = self.loop.run_until_complete(UnsubscribePacket.from_stream(stream)) self.assertEqual(message.payload.topics[0], 'a/b') self.assertEqual(message.payload.topics[1], 'c/d') def test_to_stream(self): variable_header = PacketIdVariableHeader(10) payload = UnubscribePayload(['a/b', 'c/d']) publish = UnsubscribePacket(variable_header=variable_header, payload=payload) out = publish.to_bytes() self.assertEqual(out, b'\xa2\x0c\x00\n\x00\x03a/b\x00\x03c/d') hbmqtt-0.9/tests/plugins/0000755000076500000240000000000013114340722015554 5ustar nicostaff00000000000000hbmqtt-0.9/tests/plugins/__init__.py0000644000076500000240000000002413114237777017700 0ustar nicostaff00000000000000__author__ = 'nico' hbmqtt-0.9/tests/plugins/test_authentication.py0000644000076500000240000000705413114237777022231 0ustar nicostaff00000000000000# Copyright (c) 2015 Nicolas JOUANIN # # See the file license.txt for copying permission. import unittest import logging import os import asyncio from hbmqtt.plugins.manager import BaseContext from hbmqtt.plugins.authentication import AnonymousAuthPlugin, FileAuthPlugin from hbmqtt.session import Session formatter = "[%(asctime)s] %(name)s {%(filename)s:%(lineno)d} %(levelname)s - %(message)s" logging.basicConfig(level=logging.DEBUG, format=formatter) class TestAnonymousAuthPlugin(unittest.TestCase): def setUp(self): self.loop = asyncio.new_event_loop() def test_allow_anonymous(self): context = BaseContext() context.logger = logging.getLogger(__name__) context.config = { 'auth': { 'allow-anonymous': True } } s = Session() s.username = "" auth_plugin = AnonymousAuthPlugin(context) ret = self.loop.run_until_complete(auth_plugin.authenticate(session=s)) self.assertTrue(ret) def test_disallow_anonymous(self): context = BaseContext() context.logger = logging.getLogger(__name__) context.config = { 'auth': { 'allow-anonymous': False } } s = Session() s.username = "" auth_plugin = AnonymousAuthPlugin(context) ret = self.loop.run_until_complete(auth_plugin.authenticate(session=s)) self.assertFalse(ret) def test_allow_nonanonymous(self): context = BaseContext() context.logger = logging.getLogger(__name__) context.config = { 'auth': { 'allow-anonymous': False } } s = Session() s.username = "test" auth_plugin = AnonymousAuthPlugin(context) ret = self.loop.run_until_complete(auth_plugin.authenticate(session=s)) self.assertTrue(ret) class TestFileAuthPlugin(unittest.TestCase): def setUp(self): self.loop = asyncio.new_event_loop() def test_allow(self): context = BaseContext() context.logger = logging.getLogger(__name__) context.config = { 'auth': { 'password-file': os.path.join(os.path.dirname(os.path.realpath(__file__)), "passwd") } } s = Session() s.username = "user" s.password = "test" auth_plugin = FileAuthPlugin(context) ret = self.loop.run_until_complete(auth_plugin.authenticate(session=s)) self.assertTrue(ret) def test_wrong_password(self): context = BaseContext() context.logger = logging.getLogger(__name__) context.config = { 'auth': { 'password-file': os.path.join(os.path.dirname(os.path.realpath(__file__)), "passwd") } } s = Session() s.username = "user" s.password = "wrong password" auth_plugin = FileAuthPlugin(context) ret = self.loop.run_until_complete(auth_plugin.authenticate(session=s)) self.assertFalse(ret) def test_unknown_password(self): context = BaseContext() context.logger = logging.getLogger(__name__) context.config = { 'auth': { 'password-file': os.path.join(os.path.dirname(os.path.realpath(__file__)), "passwd") } } s = Session() s.username = "some user" s.password = "some password" auth_plugin = FileAuthPlugin(context) ret = self.loop.run_until_complete(auth_plugin.authenticate(session=s)) self.assertFalse(ret) hbmqtt-0.9/tests/plugins/test_manager.py0000644000076500000240000000636013114237777020623 0ustar nicostaff00000000000000# Copyright (c) 2015 Nicolas JOUANIN # # See the file license.txt for copying permission. import unittest import logging import asyncio from hbmqtt.plugins.manager import PluginManager, BaseContext formatter = "[%(asctime)s] %(name)s {%(filename)s:%(lineno)d} %(levelname)s - %(message)s" logging.basicConfig(level=logging.INFO, format=formatter) class TestPlugin: def __init__(self, context): self.context = context class EventTestPlugin: def __init__(self, context): self.context = context self.test_flag = False self.coro_flag = False @asyncio.coroutine def on_test(self, *args, **kwargs): self.test_flag = True self.context.logger.info("on_test") @asyncio.coroutine def test_coro(self, *args, **kwargs): self.coro_flag = True @asyncio.coroutine def ret_coro(self, *args, **kwargs): return "TEST" class TestPluginManager(unittest.TestCase): def setUp(self): self.loop = asyncio.new_event_loop() def test_load_plugin(self): manager = PluginManager("hbmqtt.test.plugins", context=None) self.assertTrue(len(manager._plugins) > 0) def test_fire_event(self): @asyncio.coroutine def fire_event(): yield from manager.fire_event("test") yield from asyncio.sleep(1, loop=self.loop) yield from manager.close() manager = PluginManager("hbmqtt.test.plugins", context=None, loop=self.loop) self.loop.run_until_complete(fire_event()) plugin = manager.get_plugin("event_plugin") self.assertTrue(plugin.object.test_flag) def test_fire_event_wait(self): @asyncio.coroutine def fire_event(): yield from manager.fire_event("test", wait=True) yield from manager.close() manager = PluginManager("hbmqtt.test.plugins", context=None, loop=self.loop) self.loop.run_until_complete(fire_event()) plugin = manager.get_plugin("event_plugin") self.assertTrue(plugin.object.test_flag) def test_map_coro(self): @asyncio.coroutine def call_coro(): yield from manager.map_plugin_coro('test_coro') manager = PluginManager("hbmqtt.test.plugins", context=None, loop=self.loop) self.loop.run_until_complete(call_coro()) plugin = manager.get_plugin("event_plugin") self.assertTrue(plugin.object.test_coro) def test_map_coro_return(self): @asyncio.coroutine def call_coro(): return (yield from manager.map_plugin_coro('ret_coro')) manager = PluginManager("hbmqtt.test.plugins", context=None, loop=self.loop) ret = self.loop.run_until_complete(call_coro()) plugin = manager.get_plugin("event_plugin") self.assertEqual(ret[plugin], "TEST") def test_map_coro_filter(self): """ Run plugin coro but expect no return as an empty filter is given :return: """ @asyncio.coroutine def call_coro(): return (yield from manager.map_plugin_coro('ret_coro', filter_plugins=[])) manager = PluginManager("hbmqtt.test.plugins", context=None, loop=self.loop) ret = self.loop.run_until_complete(call_coro()) self.assertTrue(len(ret) == 0) hbmqtt-0.9/tests/plugins/test_persistence.py0000644000076500000240000000372513114237777021537 0ustar nicostaff00000000000000# Copyright (c) 2015 Nicolas JOUANIN # # See the file license.txt for copying permission. import unittest import logging import os import asyncio import sqlite3 from hbmqtt.plugins.manager import BaseContext from hbmqtt.plugins.persistence import SQLitePlugin from hbmqtt.session import Session formatter = "[%(asctime)s] %(name)s {%(filename)s:%(lineno)d} %(levelname)s - %(message)s" logging.basicConfig(level=logging.DEBUG, format=formatter) class TestSQLitePlugin(unittest.TestCase): def setUp(self): self.loop = asyncio.new_event_loop() def test_create_tables(self): dbfile = os.path.join(os.path.dirname(os.path.realpath(__file__)), "test.db") context = BaseContext() context.logger = logging.getLogger(__name__) context.config = { 'persistence': { 'file': dbfile } } sql_plugin = SQLitePlugin(context) conn = sqlite3.connect(dbfile) cursor = conn.cursor() rows = cursor.execute("SELECT name FROM sqlite_master where type = 'table'") tables = [] for row in rows: tables.append(row[0]) self.assertIn("session", tables) # def test_save_session(self): # dbfile = os.path.join(os.path.dirname(os.path.realpath(__file__)), "test.db") # context = BaseContext() # context.logger = logging.getLogger(__name__) # context.config = { # 'persistence': { # 'file': dbfile # } # } # sql_plugin = SQLitePlugin(context) # s = Session() # s.client_id = 'test_save_session' # ret = self.loop.run_until_complete(sql_plugin.save_session(session=s)) # # conn = sqlite3.connect(dbfile) # cursor = conn.cursor() # row = cursor.execute("SELECT client_id FROM session where client_id = 'test_save_session'").fetchone() # self.assertTrue(len(row) == 1) # self.assertEquals(row[0], s.client_id)