├── .gitignore ├── .travis.yml ├── MANIFEST.in ├── docs ├── Makefile ├── _static │ └── theme_overrides.css ├── _templates │ └── layout.html ├── changelog.rst ├── conf.py ├── index.rst ├── license.rst ├── quickstart.rst └── references │ ├── broker.rst │ ├── common.rst │ ├── hbmqtt.rst │ ├── hbmqtt_pub.rst │ ├── hbmqtt_sub.rst │ ├── index.rst │ └── mqttclient.rst ├── hbmqtt ├── __init__.py ├── adapters.py ├── broker.py ├── client.py ├── codecs.py ├── errors.py ├── mqtt │ ├── __init__.py │ ├── connack.py │ ├── connect.py │ ├── constants.py │ ├── disconnect.py │ ├── packet.py │ ├── pingreq.py │ ├── pingresp.py │ ├── protocol │ │ ├── __init__.py │ │ ├── broker_handler.py │ │ ├── client_handler.py │ │ └── handler.py │ ├── puback.py │ ├── pubcomp.py │ ├── publish.py │ ├── pubrec.py │ ├── pubrel.py │ ├── suback.py │ ├── subscribe.py │ ├── unsuback.py │ └── unsubscribe.py ├── plugins │ ├── __init__.py │ ├── authentication.py │ ├── logging.py │ ├── manager.py │ ├── persistence.py │ ├── sys │ │ ├── __init__.py │ │ └── broker.py │ └── topic_checking.py ├── session.py ├── utils.py └── version.py ├── license.txt ├── readme.rst ├── samples ├── broker_acl.py ├── broker_start.py ├── broker_taboo.py ├── client_keepalive.py ├── client_publish.py ├── client_publish_acl.py ├── client_publish_ssl.py ├── client_publish_ws.py ├── client_subscribe.py ├── client_subscribe_acl.py ├── mosquitto.org.crt └── passwd ├── scripts ├── __init__.py ├── broker_script.py ├── default_broker.yaml ├── default_client.yaml ├── pub_script.py └── sub_script.py ├── setup.cfg ├── setup.py ├── tests ├── __init__.py ├── mosquitto.org.crt ├── mqtt │ ├── __init__.py │ ├── protocol │ │ ├── __init__.py │ │ └── test_handler.py │ ├── test_connect.py │ ├── test_packet.py │ ├── test_puback.py │ ├── test_pubcomp.py │ ├── test_publish.py │ ├── test_pubrec.py │ ├── test_pubrel.py │ ├── test_suback.py │ ├── test_subscribe.py │ ├── test_unsuback.py │ └── test_unsubscribe.py ├── plugins │ ├── __init__.py │ ├── passwd │ ├── test_authentication.py │ ├── test_manager.py │ └── test_persistence.py ├── test_broker.py ├── test_client.py └── test_codecs.py └── tox.ini /.gitignore: -------------------------------------------------------------------------------- 1 | *.egg-info 2 | *.pyc 3 | .idea/hbmqtt.iml 4 | .tox/ 5 | __pycache__ 6 | build/ 7 | dist/ 8 | tests/plugins/test.db 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: trusty 2 | sudo: false 3 | language: python 4 | 5 | python: 6 | - "3.4" 7 | - "3.5" 8 | - "3.6" 9 | - "nightly" 10 | matrix: 11 | allow_failures: 12 | - python: "nightly" 13 | include: 14 | - python: "3.7" 15 | env: TOXENV=py37 16 | dist: xenial 17 | sudo: true 18 | 19 | install: 20 | - pip install coveralls tox-travis 21 | script: 22 | - tox 23 | after_success: 24 | - coveralls 25 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include *.rst 2 | include *.txt 3 | include license.txt 4 | include samples/passwd 5 | include tests/plugins/passwd 6 | include tox.ini 7 | 8 | recursive-include docs *.css 9 | recursive-include docs *.html 10 | recursive-include docs *.py 11 | recursive-include docs *.rst 12 | recursive-include docs Makefile 13 | recursive-include samples *.crt 14 | recursive-include samples *.py 15 | recursive-include scripts *.yaml 16 | recursive-include tests *.crt 17 | recursive-include tests *.py 18 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # User-friendly check for sphinx-build 11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) 12 | $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) 13 | endif 14 | 15 | # Internal variables. 16 | PAPEROPT_a4 = -D latex_paper_size=a4 17 | PAPEROPT_letter = -D latex_paper_size=letter 18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 19 | # the i18n builder cannot share the environment and doctrees with the others 20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 21 | 22 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest coverage gettext 23 | 24 | help: 25 | @echo "Please use \`make ' where is one of" 26 | @echo " html to make standalone HTML files" 27 | @echo " dirhtml to make HTML files named index.html in directories" 28 | @echo " singlehtml to make a single large HTML file" 29 | @echo " pickle to make pickle files" 30 | @echo " json to make JSON files" 31 | @echo " htmlhelp to make HTML files and a HTML help project" 32 | @echo " qthelp to make HTML files and a qthelp project" 33 | @echo " applehelp to make an Apple Help Book" 34 | @echo " devhelp to make HTML files and a Devhelp project" 35 | @echo " epub to make an epub" 36 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 37 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 38 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 39 | @echo " text to make text files" 40 | @echo " man to make manual pages" 41 | @echo " texinfo to make Texinfo files" 42 | @echo " info to make Texinfo files and run them through makeinfo" 43 | @echo " gettext to make PO message catalogs" 44 | @echo " changes to make an overview of all changed/added/deprecated items" 45 | @echo " xml to make Docutils-native XML files" 46 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 47 | @echo " linkcheck to check all external links for integrity" 48 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 49 | @echo " coverage to run coverage check of the documentation (if enabled)" 50 | 51 | clean: 52 | rm -rf $(BUILDDIR)/* 53 | 54 | html: 55 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 56 | @echo 57 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 58 | 59 | dirhtml: 60 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 61 | @echo 62 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 63 | 64 | singlehtml: 65 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 66 | @echo 67 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 68 | 69 | pickle: 70 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 71 | @echo 72 | @echo "Build finished; now you can process the pickle files." 73 | 74 | json: 75 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 76 | @echo 77 | @echo "Build finished; now you can process the JSON files." 78 | 79 | htmlhelp: 80 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 81 | @echo 82 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 83 | ".hhp project file in $(BUILDDIR)/htmlhelp." 84 | 85 | qthelp: 86 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 87 | @echo 88 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 89 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 90 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/HBMQTT.qhcp" 91 | @echo "To view the help file:" 92 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/HBMQTT.qhc" 93 | 94 | applehelp: 95 | $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp 96 | @echo 97 | @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." 98 | @echo "N.B. You won't be able to view it unless you put it in" \ 99 | "~/Library/Documentation/Help or install it in your application" \ 100 | "bundle." 101 | 102 | devhelp: 103 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 104 | @echo 105 | @echo "Build finished." 106 | @echo "To view the help file:" 107 | @echo "# mkdir -p $$HOME/.local/share/devhelp/HBMQTT" 108 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/HBMQTT" 109 | @echo "# devhelp" 110 | 111 | epub: 112 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 113 | @echo 114 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 115 | 116 | latex: 117 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 118 | @echo 119 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 120 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 121 | "(use \`make latexpdf' here to do that automatically)." 122 | 123 | latexpdf: 124 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 125 | @echo "Running LaTeX files through pdflatex..." 126 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 127 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 128 | 129 | latexpdfja: 130 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 131 | @echo "Running LaTeX files through platex and dvipdfmx..." 132 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 133 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 134 | 135 | text: 136 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 137 | @echo 138 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 139 | 140 | man: 141 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 142 | @echo 143 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 144 | 145 | texinfo: 146 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 147 | @echo 148 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 149 | @echo "Run \`make' in that directory to run these through makeinfo" \ 150 | "(use \`make info' here to do that automatically)." 151 | 152 | info: 153 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 154 | @echo "Running Texinfo files through makeinfo..." 155 | make -C $(BUILDDIR)/texinfo info 156 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 157 | 158 | gettext: 159 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 160 | @echo 161 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 162 | 163 | changes: 164 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 165 | @echo 166 | @echo "The overview file is in $(BUILDDIR)/changes." 167 | 168 | linkcheck: 169 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 170 | @echo 171 | @echo "Link check complete; look for any errors in the above output " \ 172 | "or in $(BUILDDIR)/linkcheck/output.txt." 173 | 174 | doctest: 175 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 176 | @echo "Testing of doctests in the sources finished, look at the " \ 177 | "results in $(BUILDDIR)/doctest/output.txt." 178 | 179 | coverage: 180 | $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage 181 | @echo "Testing of coverage in the sources finished, look at the " \ 182 | "results in $(BUILDDIR)/coverage/python.txt." 183 | 184 | xml: 185 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 186 | @echo 187 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 188 | 189 | pseudoxml: 190 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 191 | @echo 192 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 193 | -------------------------------------------------------------------------------- /docs/_static/theme_overrides.css: -------------------------------------------------------------------------------- 1 | .wy-nav-content { 2 | max-width: 1100px 3 | } -------------------------------------------------------------------------------- /docs/_templates/layout.html: -------------------------------------------------------------------------------- 1 | {% extends "!layout.html" %} 2 | 3 | {% block footer %} 4 | {{ super() }} 5 | 6 | 18 | 19 | 20 | {% endblock %} 21 | -------------------------------------------------------------------------------- /docs/changelog.rst: -------------------------------------------------------------------------------- 1 | Changelog 2 | --------- 3 | 4 | 0.9.5 5 | ..... 6 | 7 | * fix `more issues `_ 8 | * fix a `few issues `_ 9 | 10 | 0.9.2 11 | ..... 12 | 13 | * fix a `few issues `_ 14 | 15 | 16 | 0.9.1 17 | ..... 18 | 19 | * See commit log 20 | 21 | 22 | 0.9.0 23 | ..... 24 | 25 | * fix a `serie of issues `_ 26 | * improve plugin performance 27 | * support Python 3.6 28 | * upgrade to ``websockets`` 3.3.0 29 | 30 | 0.8.0 31 | ..... 32 | 33 | * fix a `serie of issues `_ 34 | 35 | 0.7.3 36 | ..... 37 | 38 | * fix deliver message client method to raise TimeoutError (`#40 `_) 39 | * fix topic filter matching in broker (`#41 `_) 40 | 41 | Version 0.7.2 has been jumped due to troubles with pypi... 42 | 43 | 0.7.1 44 | ..... 45 | 46 | * Fix `duplicated $SYS topic name `_ . 47 | 48 | 0.7.0 49 | ..... 50 | 51 | * Fix a `serie of issues `_ reported by `Christoph Krey `_ 52 | 53 | 0.6.3 54 | ..... 55 | 56 | * Fix issue `#22 `_. 57 | 58 | 0.6.2 59 | ..... 60 | 61 | * Fix issue `#20 `_ (``mqtt`` subprotocol was missing). 62 | * Upgrade to ``websockets`` 3.0. 63 | 64 | 0.6.1 65 | ..... 66 | 67 | * Fix issue `#19 `_ 68 | 69 | 0.6 70 | ... 71 | 72 | * Added compatibility with Python 3.5. 73 | * Rewritten documentation. 74 | * Add command-line tools :doc:`references/hbmqtt`, :doc:`references/hbmqtt_pub` and :doc:`references/hbmqtt_sub`. -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | HBMQTT 2 | ====== 3 | 4 | ``HBMQTT`` is an open source `MQTT`_ client and broker implementation. 5 | 6 | Built on top of :mod:`asyncio`, Python's standard asynchronous I/O framework, HBMQTT provides a straightforward API 7 | based on coroutines, making it easy to write highly concurrent applications. 8 | 9 | Features 10 | -------- 11 | 12 | HBMQTT implements the full set of `MQTT 3.1.1`_ protocol specifications and provides the following features: 13 | 14 | - Support QoS 0, QoS 1 and QoS 2 messages flow 15 | - Client auto-reconnection on network lost 16 | - Authentication through password file (more methods can be added through a plugin system) 17 | - Basic ``$SYS`` topics 18 | - TCP and websocket support 19 | - SSL support over TCP and websocket 20 | - Plugin system 21 | 22 | 23 | Requirements 24 | ------------ 25 | 26 | HBMQTT is built on Python :mod:`asyncio` library which was introduced in Python 3.4. Tests have shown that HBMQTT run 27 | best with Python 3.4.3. Python 3.5.0 is also fully supported and recommended. Make sure you use one of these version 28 | before installing HBMQTT. 29 | 30 | Installation 31 | ------------ 32 | 33 | It is not recommended to install third-party library in Python system packages directory. The preferred way for 34 | installing HBMQTT is to create a virtual environment and then install all the dependencies you need. Refer to 35 | `PEP 405`_ to learn more. 36 | 37 | Once you have a environment setup and ready, HBMQTT can be installed with the following command :: 38 | 39 | (venv) $ pip install hbmqtt 40 | 41 | ``pip`` will download and install HBMQTT and all its dependencies. 42 | 43 | 44 | User guide 45 | ---------- 46 | 47 | If you need HBMQTT for running a MQTT client or deploying a MQTT broker, the :doc:`quickstart` describes how to use console scripts provided by HBMQTT. 48 | 49 | If you want to develop an application which needs to connect to a MQTT broker, the :doc:`references/mqttclient` documentation explains how to use HBMQTT API for connecting, publishing and subscribing with a MQTT broker. 50 | 51 | If you want to run you own MQTT broker, th :doc:`references/broker` reference documentation explains how to embed a MQTT broker inside a Python application. 52 | 53 | News and updates are listed in the :doc:`changelog`. 54 | 55 | 56 | .. _MQTT: http://www.mqtt.org 57 | .. _MQTT 3.1.1: http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html 58 | .. _PEP 405: https://www.python.org/dev/peps/pep-0405/ 59 | 60 | .. toctree:: 61 | :maxdepth: 2 62 | :hidden: 63 | 64 | quickstart 65 | changelog 66 | references/index 67 | license 68 | 69 | -------------------------------------------------------------------------------- /docs/license.rst: -------------------------------------------------------------------------------- 1 | License 2 | ------- 3 | 4 | .. literalinclude:: ../license.txt 5 | -------------------------------------------------------------------------------- /docs/quickstart.rst: -------------------------------------------------------------------------------- 1 | Quickstart 2 | ========== 3 | 4 | A quick way for getting started with ``HBMQTT`` is to use console scripts provided for : 5 | 6 | * publishing a message on some topic on an external MQTT broker. 7 | * subscribing some topics and getting published messages. 8 | * running a autonomous MQTT broker 9 | 10 | These scripts are installed automatically when installing ``HBMQTT`` with the following command :: 11 | 12 | (venv) $ pip install hbmqtt 13 | 14 | Publishing messages 15 | ------------------- 16 | 17 | ``hbmqtt_pub`` is a command-line tool which can be used for publishing some messages on a topic. It requires a few arguments like broker URL, topic name, QoS and data to send. Additional options allow more complex use case. 18 | 19 | Publishing ```some_data`` to as ``/test`` topic on is as simple as : 20 | :: 21 | 22 | $ hbmqtt_pub --url mqtt://test.mosquitto.org -t /test -m some_data 23 | [2015-11-06 22:21:55,108] :: INFO - hbmqtt_pub/5135-MacBook-Pro.local Connecting to broker 24 | [2015-11-06 22:21:55,333] :: INFO - hbmqtt_pub/5135-MacBook-Pro.local Publishing to '/test' 25 | [2015-11-06 22:21:55,336] :: INFO - hbmqtt_pub/5135-MacBook-Pro.local Disconnected from broker 26 | 27 | This will use insecure TCP connection to connect to test.mosquitto.org. ``hbmqtt_pub`` also allows websockets and secure connection: 28 | :: 29 | 30 | $ hbmqtt_pub --url ws://test.mosquitto.org:8080 -t /test -m some_data 31 | [2015-11-06 22:22:42,542] :: INFO - hbmqtt_pub/5157-MacBook-Pro.local Connecting to broker 32 | [2015-11-06 22:22:42,924] :: INFO - hbmqtt_pub/5157-MacBook-Pro.local Publishing to '/test' 33 | [2015-11-06 22:22:52,926] :: INFO - hbmqtt_pub/5157-MacBook-Pro.local Disconnected from broker 34 | 35 | ``hbmqtt_pub`` can read from file or stdin and use data read as message payload: 36 | :: 37 | 38 | $ some_command | hbmqtt_pub --url mqtt://localhost -t /test -l 39 | 40 | See :doc:`references/hbmqtt_pub` reference documentation for details about available options and settings. 41 | 42 | Subscribing a topic 43 | ------------------- 44 | 45 | ``hbmqtt_sub`` is a command-line tool which can be used to subscribe for some pattern(s) on a broker and get date from messages published on topics matching these patterns by other MQTT clients. 46 | 47 | Subscribing a ``/test/#`` topic pattern is done with : 48 | :: 49 | 50 | $ hbmqtt_sub --url mqtt://localhost -t /test/# 51 | 52 | This command will run forever and print on the standard output every messages received from the broker. The ``-n`` option allows to set a maximum number of messages to receive before stopping. 53 | 54 | See :doc:`references/hbmqtt_sub` reference documentation for details about available options and settings. 55 | 56 | 57 | URL Scheme 58 | ---------- 59 | 60 | HBMQTT command line tools use the ``--url`` to establish a network connection with the broker. The ``--url`` parameter value must conform to the `MQTT URL scheme`_. The general accepted form is : 61 | :: 62 | 63 | [mqtt|ws][s]://[username][:password]@host.domain[:port] 64 | 65 | Here are some examples of URL: 66 | :: 67 | 68 | mqtt://localhost 69 | mqtt://localhost:1884 70 | mqtt://user:password@localhost 71 | ws://test.mosquitto.org 72 | wss://user:password@localhost 73 | 74 | .. _MQTT URL scheme: https://github.com/mqtt/mqtt.github.io/wiki/URI-Scheme 75 | 76 | 77 | Running a broker 78 | ---------------- 79 | 80 | ``hbmqtt`` is a command-line tool for running a MQTT broker: 81 | :: 82 | 83 | $ hbmqtt 84 | [2015-11-06 22:45:16,470] :: INFO - Listener 'default' bind to 0.0.0.0:1883 (max_connections=-1) 85 | 86 | See :doc:`references/hbmqtt` reference documentation for details about available options and settings. 87 | -------------------------------------------------------------------------------- /docs/references/broker.rst: -------------------------------------------------------------------------------- 1 | Broker API reference 2 | ==================== 3 | 4 | The :class:`~hbmqtt.broker.Broker` class provides a complete MQTT 3.1.1 broker implementation. This class allows Python developers to embed a MQTT broker in their own applications. 5 | 6 | Usage example 7 | ------------- 8 | 9 | The following example shows how to start a broker using the default configuration: 10 | 11 | .. code-block:: python 12 | 13 | import logging 14 | import asyncio 15 | import os 16 | from hbmqtt.broker import Broker 17 | 18 | 19 | @asyncio.coroutine 20 | def broker_coro(): 21 | broker = Broker() 22 | yield from broker.start() 23 | 24 | 25 | if __name__ == '__main__': 26 | formatter = "[%(asctime)s] :: %(levelname)s :: %(name)s :: %(message)s" 27 | logging.basicConfig(level=logging.INFO, format=formatter) 28 | asyncio.get_event_loop().run_until_complete(broker_coro()) 29 | asyncio.get_event_loop().run_forever() 30 | 31 | When executed, this script gets the default event loop and asks it to run the ``broker_coro`` until it completes. 32 | ``broker_coro`` creates :class:`~hbmqtt.broker.Broker` instance and then :meth:`~hbmqtt.broker.Broker.start` the broker for serving. 33 | Once completed, the loop is ran forever, making this script never stop ... 34 | 35 | Reference 36 | --------- 37 | 38 | Broker API 39 | .......... 40 | 41 | .. automodule:: hbmqtt.broker 42 | 43 | .. autoclass:: Broker 44 | 45 | .. automethod:: start 46 | .. automethod:: shutdown 47 | 48 | Broker configuration 49 | .................... 50 | 51 | The :class:`~hbmqtt.broker.Broker` ``__init__`` method accepts a ``config`` parameter which allow to setup some behaviour and defaults settings. This argument must be a Python dict object. For convinience, it is presented below as a YAML file [1]_. 52 | 53 | .. code-block:: python 54 | 55 | listeners: 56 | default: 57 | max-connections: 50000 58 | type: tcp 59 | my-tcp-1: 60 | bind: 127.0.0.1:1883 61 | my-tcp-2: 62 | bind: 1.2.3.4:1884 63 | max-connections: 1000 64 | my-tcp-ssl-1: 65 | bind: 127.0.0.1:8885 66 | ssl: on 67 | cafile: /some/cafile 68 | capath: /some/folder 69 | capath: certificate data 70 | certfile: /some/certfile 71 | keyfile: /some/key 72 | my-ws-1: 73 | bind: 0.0.0.0:8080 74 | type: ws 75 | timeout-disconnect-delay: 2 76 | auth: 77 | plugins: ['auth.anonymous'] #List of plugins to activate for authentication among all registered plugins 78 | allow-anonymous: true / false 79 | password-file: /some/passwd_file 80 | topic-check: 81 | enabled: true / false # Set to False if topic filtering is not needed 82 | plugins: ['topic_acl'] #List of plugins to activate for topic filtering among all registered plugins 83 | acl: 84 | # username: [list of allowed topics] 85 | username1: ['repositories/+/master', 'calendar/#', 'data/memes'] # List of topics on which client1 can publish and subscribe 86 | username2: ... 87 | anonymous: [] # List of topics on which an anonymous client can publish and subscribe 88 | 89 | 90 | The ``listeners`` section allows to define network listeners which must be started by the :class:`~hbmqtt.broker.Broker`. Several listeners can be setup. ``default`` subsection defines common attributes for all listeners. Each listener can have the following settings: 91 | 92 | * ``bind``: IP address and port binding. 93 | * ``max-connections``: Set maximum number of active connection for the listener. ``0`` means no limit. 94 | * ``type``: transport protocol type; can be ``tcp`` for classic TCP listener or ``ws`` for MQTT over websocket. 95 | * ``ssl`` enables (``on``) or disable secured connection over the transport protocol. 96 | * ``cafile``, ``cadata``, ``certfile`` and ``keyfile`` : mandatory parameters for SSL secured connections. 97 | 98 | The ``auth`` section setup authentication behaviour: 99 | 100 | * ``plugins``: defines the list of activated plugins. Note the plugins must be defined in the ``hbmqtt.broker.plugins`` `entry point `_. 101 | * ``allow-anonymous`` : used by the internal :class:`hbmqtt.plugins.authentication.AnonymousAuthPlugin` plugin. This parameter enables (``on``) or disable anonymous connection, ie. connection without username. 102 | * ``password-file`` : used by the internal :class:`hbmqtt.plugins.authentication.FileAuthPlugin` plugin. This parameter gives to path of the password file to load for authenticating users. 103 | 104 | The ``topic-check`` section setup access control policies for publishing and subscribing to topics: 105 | 106 | * ``enabled``: set to true if you want to impose an access control policy. Otherwise, set it to false. 107 | * ``plugins``: defines the list of activated plugins. Note the plugins must be defined in the ``hbmqtt.broker.plugins`` `entry point `_. 108 | * additional parameters: depending on the plugin used for access control, additional parameters should be added. 109 | * In case of ``topic_acl`` plugin, the Access Control List (ACL) must be defined in the parameter ``acl``. 110 | * For each username, a list with the allowed topics must be defined. 111 | * If the client logs in anonymously, the ``anonymous`` entry within the ACL is used in order to grant/deny subscriptions. 112 | 113 | 114 | .. [1] See `PyYAML `_ for loading YAML files as Python dict. 115 | -------------------------------------------------------------------------------- /docs/references/common.rst: -------------------------------------------------------------------------------- 1 | Common API 2 | ========== 3 | 4 | This document describes ``HBMQTT`` common API both used by :doc:`mqttclient` and :doc:`broker`. 5 | 6 | Reference 7 | --------- 8 | 9 | ApplicationMessage 10 | .................. 11 | 12 | .. automodule:: hbmqtt.session 13 | 14 | .. autoclass:: ApplicationMessage 15 | :members: 16 | 17 | .. autoclass:: IncomingApplicationMessage 18 | :show-inheritance: 19 | 20 | .. autoclass:: OutgoingApplicationMessage 21 | :show-inheritance: 22 | -------------------------------------------------------------------------------- /docs/references/hbmqtt.rst: -------------------------------------------------------------------------------- 1 | hbmqtt 2 | ====== 3 | 4 | ``hbmqtt`` is a command-line script for running a MQTT 3.1.1 broker. 5 | 6 | Usage 7 | ----- 8 | 9 | ``hbmqtt`` usage : 10 | :: 11 | 12 | hbmqtt --version 13 | hbmqtt (-h | --help) 14 | hbmqtt [-c ] [-d] 15 | 16 | 17 | Options 18 | ------- 19 | 20 | --version HBMQTT version information 21 | -h, --help Display ``hbmqtt_sub`` usage help 22 | -c Set the YAML configuration file to read and pass to the client runtime. 23 | 24 | 25 | Configuration 26 | ------------- 27 | 28 | If ``-c`` argument is given, ``hbmqtt`` will read specific MQTT settings for the given configuration file. This file must be a valid `YAML`_ file which may contains the following configuration elements : 29 | 30 | * ``listeners`` : network bindings configuration list 31 | * ``timeout-disconnect-delay`` : client disconnect timeout after keep-alive timeout 32 | * ``auth`` : authentication configuration 33 | 34 | Without the ``-c`` argument, the broker will run with the following default configuration: 35 | 36 | .. code-block:: yaml 37 | 38 | listeners: 39 | default: 40 | type: tcp 41 | bind: 0.0.0.0:1883 42 | sys_interval: 20 43 | auth: 44 | allow-anonymous: true 45 | plugins: 46 | - auth_file 47 | - auth_anonymous 48 | 49 | Using this configuration, ``hbmqtt`` will start a broker : 50 | 51 | * listening on TCP port 1883 on all network interfaces. 52 | * Publishing ``$SYS``_ update messages every ``20`` seconds. 53 | * Allowing anonymous login, and password file bases authentication. 54 | 55 | .. _YAML: http://yaml.org/ 56 | 57 | 58 | Configuration example 59 | --------------------- 60 | 61 | .. code-block:: yaml 62 | 63 | listeners: 64 | default: 65 | max-connections: 50000 66 | type: tcp 67 | my-tcp-1: 68 | bind: 127.0.0.1:1883 69 | my-tcp-2: 70 | bind: 1.2.3.4:1883 71 | max-connections: 1000 72 | my-tcp-ssl-1: 73 | bind: 127.0.0.1:8883 74 | ssl: on 75 | cafile: /some/cafile 76 | capath: /some/folder 77 | capath: certificate data 78 | certfile: /some/certfile 79 | keyfile: /some/key 80 | my-ws-1: 81 | bind: 0.0.0.0:8080 82 | type: ws 83 | timeout-disconnect-delay: 2 84 | auth: 85 | plugins: ['auth.anonymous'] 86 | allow-anonymous: true 87 | password-file: /some/passwd_file 88 | 89 | This configuration example shows use case of every parameter. 90 | 91 | The ``listeners`` section define 3 bindings : 92 | 93 | * ``my-tcp-1`` : a unsecured TCP listener on port 1883 allowing ``1000`` clients connections simultaneously 94 | * ``my-tcp-ssl-1`` : a secured TCP listener on port 8883 allowing ``50000`` clients connections simultaneously 95 | * ``my-ws-1`` : a unsecured websocket listener on port 8080 allowing ``50000`` clients connections simultaneously 96 | 97 | Authentication allows anonymous logins and password file based authentication. Password files are required to be text files containing user name and password in the form of : 98 | :: 99 | 100 | username:password 101 | 102 | where ``password`` should be the encrypted password. Use the ``mkpasswd -m sha-512`` command to build encoded passphrase. Password file example: 103 | :: 104 | 105 | # Test user with 'test' password encrypted with sha-512 106 | test:$6$l4zQEHEcowc1Pnv4$HHrh8xnsZoLItQ8BmpFHM4r6q5UqK3DnXp2GaTm5zp5buQ7NheY3Xt9f6godVKbEtA.hOC7IEDwnok3pbAOip. 107 | -------------------------------------------------------------------------------- /docs/references/hbmqtt_pub.rst: -------------------------------------------------------------------------------- 1 | hbmqtt_pub 2 | ========== 3 | 4 | ``hbmqtt_pub`` is a MQTT client that publishes simple messages on a topic from the command line. 5 | 6 | Usage 7 | ----- 8 | 9 | ``hbmqtt_pub`` usage : :: 10 | 11 | hbmqtt_pub --version 12 | hbmqtt_pub (-h | --help) 13 | hbmqtt_pub --url BROKER_URL -t TOPIC (-f FILE | -l | -m MESSAGE | -n | -s) [-c CONFIG_FILE] [-i CLIENT_ID] [-d] 14 | [-q | --qos QOS] [-d] [-k KEEP_ALIVE] [--clean-session] 15 | [--ca-file CAFILE] [--ca-path CAPATH] [--ca-data CADATA] 16 | [ --will-topic WILL_TOPIC [--will-message WILL_MESSAGE] [--will-qos WILL_QOS] [--will-retain] ] 17 | [--extra-headers HEADER] 18 | 19 | Note that for simplicity, ``hbmqtt_pub`` uses mostly the same argument syntax as `mosquitto_pub`_. 20 | 21 | .. _mosquitto_pub: http://mosquitto.org/man/mosquitto_pub-1.html 22 | 23 | Options 24 | ------- 25 | 26 | --version HBMQTT version information 27 | -h, --help Display ``hbmqtt_pub`` usage help 28 | -c Set the YAML configuration file to read and pass to the client runtime. 29 | -d Enable debugging informations. 30 | --ca-file Define the path to a file containing PEM encoded CA certificates that are trusted. Used to enable SSL communication. 31 | --ca-path Define the path to a directory containing PEM encoded CA certificates that are trusted. Used to enable SSL communication. 32 | --ca-data Set the PEM encoded CA certificates that are trusted. Used to enable SSL communication. 33 | --clean-session If given, set the CONNECT clean session flag to True. 34 | -f Send the contents of a file as the message. The file is read line by line, and ``hbmqtt_pub`` will publish a message for each line read. 35 | -i The id to use for this client. If not given, defaults to ``hbmqtt_pub/`` appended with the process id and the hostname of the client. 36 | -l Send messages read from stdin. ``hbmqtt_pub`` will publish a message for each line read. Blank lines won't be sent. 37 | -k Set the CONNECT keep alive timeout. 38 | -m Send a single message from the command line. 39 | -n Send a null (zero length) message. 40 | -q, --qos Specify the quality of service to use for the message, from 0, 1 and 2. Defaults to 0. 41 | -s Send a message read from stdin, sending the entire content as a single message. 42 | -t The MQTT topic on which to publish the message. 43 | --url Broker connection URL, conforming to `MQTT URL scheme`_. 44 | --will-topic The topic on which to send a Will, in the event that the client disconnects unexpectedly. 45 | --will-message Specify a message that will be stored by the broker and sent out if this client disconnects unexpectedly. This must be used in conjunction with ``--will-topic``. 46 | --will-qos The QoS to use for the Will. Defaults to 0. This must be used in conjunction with ``--will-topic``. 47 | --will-retain If given, if the client disconnects unexpectedly the message sent out will be treated as a retained message. This must be used in conjunction with ``--will-topic``. 48 | --extra-headers Specify a JSON object string with key-value pairs representing additional headers that are transmitted on the initial connection, but only when using a websocket connection 49 | 50 | 51 | .. _MQTT URL scheme: https://github.com/mqtt/mqtt.github.io/wiki/URI-Scheme 52 | 53 | 54 | Configuration 55 | ------------- 56 | 57 | If ``-c`` argument is given, ``hbmqtt_pub`` will read specific MQTT settings for the given configuration file. This file must be a valid `YAML`_ file which may contains the following configuration elements : 58 | 59 | * ``keep_alive`` : Keep-alive timeout sent to the broker. Defaults to ``10`` seconds. 60 | * ``ping_delay`` : Auto-ping delay before keep-alive timeout. Defaults to 1. Setting to ``0`` will disable to 0 and may lead to broker disconnection. 61 | * ``default_qos`` : Default QoS for messages published. Defaults to 0. 62 | * ``default_retain`` : Default retain value to messages published. Defaults to ``false``. 63 | * ``auto_reconnect`` : Enable or disable auto-reconnect if connectection with the broker is interrupted. Defaults to ``false``. 64 | * ``reconnect_retries`` : Maximum reconnection retries. Defaults to ``2``. Negative value will cause client to reconnect infinietly. 65 | * ``reconnect_max_interval`` : Maximum interval between 2 connection retry. Defaults to ``10``. 66 | 67 | 68 | .. _YAML: http://yaml.org/ 69 | 70 | Examples 71 | -------- 72 | 73 | Examples below are adapted from `mosquitto_pub`_ documentation. 74 | 75 | 76 | Publish temperature information to localhost with QoS 1: 77 | :: 78 | 79 | hbmqtt_pub --url mqtt://localhost -t sensors/temperature -m 32 -q 1 80 | 81 | Publish timestamp and temperature information to a remote host on a non-standard port and QoS 0: 82 | :: 83 | 84 | hbmqtt_pub --url mqtt://192.168.1.1:1885 -t sensors/temperature -m "1266193804 32" 85 | 86 | Publish light switch status. Message is set to retained because there may be a long period of time between light switch events: 87 | :: 88 | 89 | hbmqtt_pub --url mqtt://localhost -r -t switches/kitchen_lights/status -m "on" 90 | 91 | Send the contents of a file in two ways: 92 | :: 93 | 94 | hbmqtt_pub --url mqtt://localhost -t my/topic -f ./data 95 | 96 | hbmqtt_pub --url mqtt://localhost -t my/topic -s < ./data 97 | 98 | Publish temperature information to localhost with QoS 1 over mqtt encapsulated in a websocket connection and additional headers: 99 | :: 100 | 101 | hbmqtt_pub --url wss://localhost -t sensors/temperature -m 32 -q 1 --extra-headers '{"Authorization": "Bearer "}' 102 | 103 | 104 | .. _mosquitto_pub : http://mosquitto.org/man/mosquitto_pub-1.html 105 | 106 | -------------------------------------------------------------------------------- /docs/references/hbmqtt_sub.rst: -------------------------------------------------------------------------------- 1 | hbmqtt_sub 2 | ========== 3 | 4 | ``hbmqtt_sub`` is a command line MQTT client that subscribes to some topics and output data received from messages published. 5 | 6 | Usage 7 | ----- 8 | 9 | ``hbmqtt_sub`` usage : :: 10 | 11 | hbmqtt_sub --version 12 | hbmqtt_sub (-h | --help) 13 | hbmqtt_sub --url BROKER_URL -t TOPIC... [-n COUNT] [-c CONFIG_FILE] [-i CLIENT_ID] [-q | --qos QOS] [-d] 14 | [-k KEEP_ALIVE] [--clean-session] [--ca-file CAFILE] [--ca-path CAPATH] [--ca-data CADATA] 15 | [ --will-topic WILL_TOPIC [--will-message WILL_MESSAGE] [--will-qos WILL_QOS] [--will-retain] ] 16 | [--extra-headers HEADER] 17 | 18 | Note that for simplicity, ``hbmqtt_sub`` uses mostly the same argument syntax as `mosquitto_sub`_. 19 | 20 | Options 21 | ------- 22 | 23 | --version HBMQTT version information 24 | -h, --help Display ``hbmqtt_sub`` usage help 25 | -c Set the YAML configuration file to read and pass to the client runtime. 26 | -d Enable debugging informations. 27 | --ca-file Define the path to a file containing PEM encoded CA certificates that are trusted. Used to enable SSL communication. 28 | --ca-path Define the path to a directory containing PEM encoded CA certificates that are trusted. Used to enable SSL communication. 29 | --ca-data Set the PEM encoded CA certificates that are trusted. Used to enable SSL communication. 30 | --clean-session If given, set the CONNECT clean session flag to True. 31 | -i The id to use for this client. If not given, defaults to ``hbmqtt_sub/`` appended with the process id and the hostname of the client. 32 | -k Set the CONNECT keep alive timeout. 33 | -n Number of messages to read before ending. Read forever if not given. 34 | -q, --qos Specify the quality of service to use for receiving messages. This QoS is sent in the subscribe request. 35 | -t Topic filters to subcribe. 36 | --url Broker connection URL, conforming to `MQTT URL scheme`_. 37 | --will-topic The topic on which to send a Will, in the event that the client disconnects unexpectedly. 38 | --will-message Specify a message that will be stored by the broker and sent out if this client disconnects unexpectedly. This must be used in conjunction with ``--will-topic``. 39 | --will-qos The QoS to use for the Will. Defaults to 0. This must be used in conjunction with ``--will-topic``. 40 | --will-retain If given, if the client disconnects unexpectedly the message sent out will be treated as a retained message. This must be used in conjunction with ``--will-topic``. 41 | --extra-headers Specify a JSON object string with key-value pairs representing additional headers that are transmitted on the initial connection, but only when using a websocket connection 42 | 43 | 44 | .. _MQTT URL scheme: https://github.com/mqtt/mqtt.github.io/wiki/URI-Scheme 45 | 46 | 47 | Configuration 48 | ------------- 49 | 50 | If ``-c`` argument is given, ``hbmqtt_sub`` will read specific MQTT settings for the given configuration file. This file must be a valid `YAML`_ file which may contains the following configuration elements : 51 | 52 | * ``keep_alive`` : Keep-alive timeout sent to the broker. Defaults to ``10`` seconds. 53 | * ``ping_delay`` : Auto-ping delay before keep-alive timeout. Defaults to 1. Setting to ``0`` will disable to 0 and may lead to broker disconnection. 54 | * ``default_qos`` : Default QoS for messages published. Defaults to 0. 55 | * ``default_retain`` : Default retain value to messages published. Defaults to ``false``. 56 | * ``auto_reconnect`` : Enable or disable auto-reconnect if connectection with the broker is interrupted. Defaults to ``false``. 57 | * ``reconnect_retries`` : Maximum reconnection retries. Defaults to ``2``. Negative value will cause client to reconnect infinietly. 58 | * ``reconnect_max_interval`` : Maximum interval between 2 connection retry. Defaults to ``10``. 59 | 60 | 61 | .. _YAML: http://yaml.org/ 62 | 63 | Examples 64 | -------- 65 | 66 | Examples below are adapted from `mosquitto_sub`_ documentation. 67 | 68 | 69 | Subscribe with QoS 0 to all messages published under $SYS/: 70 | :: 71 | 72 | hbmqtt_sub --url mqtt://localhost -t '$SYS/#' -q 0 73 | 74 | 75 | Subscribe to 10 messages with QoS 2 from /#: 76 | :: 77 | 78 | hbmqtt_sub --url mqtt://localhost -t /# -q 2 -n 10 79 | 80 | .. _mosquitto_sub : http://mosquitto.org/man/mosquitto_sub-1.html 81 | 82 | Subscribe with QoS 0 to all messages published under $SYS/: over mqtt encapsulated in a websocket connection and additional headers: 83 | :: 84 | 85 | hbmqtt_sub --url wss://localhost -t '$SYS/#' -q 0 --extra-headers '{"Authorization": "Bearer "}' 86 | -------------------------------------------------------------------------------- /docs/references/index.rst: -------------------------------------------------------------------------------- 1 | References 2 | ========== 3 | 4 | Reference documentation for HBMQTT console scripts and programming API. 5 | 6 | Console scripts 7 | --------------- 8 | 9 | * :doc:`hbmqtt_pub` : MQTT client for publishing messages to a broker 10 | * :doc:`hbmqtt_sub` : MQTT client for subscribing to a topics and retrieved published messages 11 | * :doc:`hbmqtt` : Autonomous MQTT broker 12 | 13 | Programming API 14 | --------------- 15 | 16 | * :doc:`mqttclient` : MQTT client API reference 17 | * :doc:`broker` : MQTT broker API reference 18 | * :doc:`common` : Common API 19 | 20 | TBD 21 | 22 | .. toctree:: 23 | :hidden: 24 | 25 | hbmqtt_pub 26 | hbmqtt_sub 27 | hbmqtt 28 | mqttclient 29 | broker 30 | common 31 | -------------------------------------------------------------------------------- /hbmqtt/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2015 Nicolas JOUANIN 2 | # 3 | # See the file license.txt for copying permission. 4 | 5 | VERSION = (0, 9, 7, 'alpha', 0) 6 | -------------------------------------------------------------------------------- /hbmqtt/adapters.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2015 Nicolas JOUANIN 2 | # 3 | # See the file license.txt for copying permission. 4 | import asyncio 5 | import io 6 | from websockets.protocol import WebSocketCommonProtocol 7 | from websockets.exceptions import ConnectionClosed 8 | from asyncio import StreamReader, StreamWriter 9 | import logging 10 | 11 | 12 | class ReaderAdapter: 13 | """ 14 | Base class for all network protocol reader adapter. 15 | 16 | Reader adapters are used to adapt read operations on the network depending on the protocol used 17 | """ 18 | 19 | @asyncio.coroutine 20 | def read(self, n=-1) -> bytes: 21 | """ 22 | Read up to n bytes. If n is not provided, or set to -1, read until EOF and return all read bytes. 23 | If the EOF was received and the internal buffer is empty, return an empty bytes object. 24 | :return: packet read as bytes data 25 | """ 26 | 27 | def feed_eof(self): 28 | """ 29 | Acknowleddge EOF 30 | """ 31 | 32 | 33 | class WriterAdapter: 34 | """ 35 | Base class for all network protocol writer adapter. 36 | 37 | Writer adapters are used to adapt write operations on the network depending on the protocol used 38 | """ 39 | 40 | def write(self, data): 41 | """ 42 | write some data to the protocol layer 43 | """ 44 | 45 | @asyncio.coroutine 46 | def drain(self): 47 | """ 48 | Let the write buffer of the underlying transport a chance to be flushed. 49 | """ 50 | 51 | def get_peer_info(self): 52 | """ 53 | Return peer socket info (remote address and remote port as tuple 54 | """ 55 | 56 | @asyncio.coroutine 57 | def close(self): 58 | """ 59 | Close the protocol connection 60 | """ 61 | 62 | 63 | class WebSocketsReader(ReaderAdapter): 64 | """ 65 | WebSockets API reader adapter 66 | This adapter relies on WebSocketCommonProtocol to read from a WebSocket. 67 | """ 68 | def __init__(self, protocol: WebSocketCommonProtocol): 69 | self._protocol = protocol 70 | self._stream = io.BytesIO(b'') 71 | 72 | @asyncio.coroutine 73 | def read(self, n=-1) -> bytes: 74 | yield from self._feed_buffer(n) 75 | data = self._stream.read(n) 76 | return data 77 | 78 | @asyncio.coroutine 79 | def _feed_buffer(self, n=1): 80 | """ 81 | Feed the data buffer by reading a Websocket message. 82 | :param n: if given, feed buffer until it contains at least n bytes 83 | """ 84 | buffer = bytearray(self._stream.read()) 85 | while len(buffer) < n: 86 | try: 87 | message = yield from self._protocol.recv() 88 | except ConnectionClosed: 89 | message = None 90 | if message is None: 91 | break 92 | if not isinstance(message, bytes): 93 | raise TypeError("message must be bytes") 94 | buffer.extend(message) 95 | self._stream = io.BytesIO(buffer) 96 | 97 | 98 | class WebSocketsWriter(WriterAdapter): 99 | """ 100 | WebSockets API writer adapter 101 | This adapter relies on WebSocketCommonProtocol to read from a WebSocket. 102 | """ 103 | def __init__(self, protocol: WebSocketCommonProtocol): 104 | self._protocol = protocol 105 | self._stream = io.BytesIO(b'') 106 | 107 | def write(self, data): 108 | """ 109 | write some data to the protocol layer 110 | """ 111 | self._stream.write(data) 112 | 113 | @asyncio.coroutine 114 | def drain(self): 115 | """ 116 | Let the write buffer of the underlying transport a chance to be flushed. 117 | """ 118 | data = self._stream.getvalue() 119 | if len(data): 120 | yield from self._protocol.send(data) 121 | self._stream = io.BytesIO(b'') 122 | 123 | def get_peer_info(self): 124 | return self._protocol.remote_address 125 | 126 | @asyncio.coroutine 127 | def close(self): 128 | yield from self._protocol.close() 129 | 130 | 131 | class StreamReaderAdapter(ReaderAdapter): 132 | """ 133 | Asyncio Streams API protocol adapter 134 | This adapter relies on StreamReader to read from a TCP socket. 135 | Because API is very close, this class is trivial 136 | """ 137 | def __init__(self, reader: StreamReader): 138 | self._reader = reader 139 | 140 | @asyncio.coroutine 141 | def read(self, n=-1) -> bytes: 142 | if n == -1: 143 | data = yield from self._reader.read(n) 144 | else: 145 | data = yield from self._reader.readexactly(n) 146 | return data 147 | 148 | def feed_eof(self): 149 | return self._reader.feed_eof() 150 | 151 | 152 | class StreamWriterAdapter(WriterAdapter): 153 | """ 154 | Asyncio Streams API protocol adapter 155 | This adapter relies on StreamWriter to write to a TCP socket. 156 | Because API is very close, this class is trivial 157 | """ 158 | def __init__(self, writer: StreamWriter): 159 | self.logger = logging.getLogger(__name__) 160 | self._writer = writer 161 | self.is_closed = False # StreamWriter has no test for closed...we use our own 162 | 163 | def write(self, data): 164 | if not self.is_closed: 165 | self._writer.write(data) 166 | 167 | @asyncio.coroutine 168 | def drain(self): 169 | if not self.is_closed: 170 | yield from self._writer.drain() 171 | 172 | def get_peer_info(self): 173 | extra_info = self._writer.get_extra_info('peername') 174 | return extra_info[0], extra_info[1] 175 | 176 | @asyncio.coroutine 177 | def close(self): 178 | if not self.is_closed: 179 | self.is_closed = True # we first mark this closed so yields below don't cause races with waiting writes 180 | yield from self._writer.drain() 181 | if self._writer.can_write_eof(): 182 | self._writer.write_eof() 183 | self._writer.close() 184 | try: yield from self._writer.wait_closed() # py37+ 185 | except AttributeError: pass 186 | 187 | 188 | class BufferReader(ReaderAdapter): 189 | """ 190 | Byte Buffer reader adapter 191 | This adapter simply adapt reading a byte buffer. 192 | """ 193 | def __init__(self, buffer: bytes): 194 | self._stream = io.BytesIO(buffer) 195 | 196 | @asyncio.coroutine 197 | def read(self, n=-1) -> bytes: 198 | return self._stream.read(n) 199 | 200 | 201 | class BufferWriter(WriterAdapter): 202 | """ 203 | ByteBuffer writer adapter 204 | This adapter simply adapt writing to a byte buffer 205 | """ 206 | def __init__(self, buffer=b''): 207 | self._stream = io.BytesIO(buffer) 208 | 209 | def write(self, data): 210 | """ 211 | write some data to the protocol layer 212 | """ 213 | self._stream.write(data) 214 | 215 | @asyncio.coroutine 216 | def drain(self): 217 | pass 218 | 219 | def get_buffer(self): 220 | return self._stream.getvalue() 221 | 222 | def get_peer_info(self): 223 | return "BufferWriter", 0 224 | 225 | @asyncio.coroutine 226 | def close(self): 227 | self._stream.close() 228 | -------------------------------------------------------------------------------- /hbmqtt/codecs.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2015 Nicolas JOUANIN 2 | # 3 | # See the file license.txt for copying permission. 4 | import asyncio 5 | from struct import pack, unpack 6 | from hbmqtt.errors import NoDataException 7 | 8 | 9 | def bytes_to_hex_str(data): 10 | """ 11 | converts a sequence of bytes into its displayable hex representation, ie: 0x?????? 12 | :param data: byte sequence 13 | :return: Hexadecimal displayable representation 14 | """ 15 | return '0x' + ''.join(format(b, '02x') for b in data) 16 | 17 | 18 | def bytes_to_int(data): 19 | """ 20 | convert a sequence of bytes to an integer using big endian byte ordering 21 | :param data: byte sequence 22 | :return: integer value 23 | """ 24 | try: 25 | return int.from_bytes(data, byteorder='big') 26 | except: 27 | return data 28 | 29 | 30 | def int_to_bytes(int_value: int, length: int) -> bytes: 31 | """ 32 | convert an integer to a sequence of bytes using big endian byte ordering 33 | :param int_value: integer value to convert 34 | :param length: (optional) byte length 35 | :return: byte sequence 36 | """ 37 | if length == 1: 38 | fmt = "!B" 39 | elif length == 2: 40 | fmt = "!H" 41 | return pack(fmt, int_value) 42 | 43 | 44 | @asyncio.coroutine 45 | def read_or_raise(reader, n=-1): 46 | """ 47 | Read a given byte number from Stream. NoDataException is raised if read gives no data 48 | :param reader: reader adapter 49 | :param n: number of bytes to read 50 | :return: bytes read 51 | """ 52 | try: 53 | data = yield from reader.read(n) 54 | except (asyncio.IncompleteReadError, ConnectionResetError, BrokenPipeError): 55 | data = None 56 | if not data: 57 | raise NoDataException("No more data") 58 | return data 59 | 60 | 61 | @asyncio.coroutine 62 | def decode_string(reader) -> bytes: 63 | """ 64 | Read a string from a reader and decode it according to MQTT string specification 65 | :param reader: Stream reader 66 | :return: UTF-8 string read from stream 67 | """ 68 | length_bytes = yield from read_or_raise(reader, 2) 69 | str_length = unpack("!H", length_bytes) 70 | if str_length[0]: 71 | byte_str = yield from read_or_raise(reader, str_length[0]) 72 | try: 73 | return byte_str.decode(encoding='utf-8') 74 | except: 75 | return str(byte_str) 76 | else: 77 | return '' 78 | 79 | 80 | @asyncio.coroutine 81 | def decode_data_with_length(reader) -> bytes: 82 | """ 83 | Read data from a reader. Data is prefixed with 2 bytes length 84 | :param reader: Stream reader 85 | :return: bytes read from stream (without length) 86 | """ 87 | length_bytes = yield from read_or_raise(reader, 2) 88 | bytes_length = unpack("!H", length_bytes) 89 | data = yield from read_or_raise(reader, bytes_length[0]) 90 | return data 91 | 92 | 93 | def encode_string(string: str) -> bytes: 94 | data = string.encode(encoding='utf-8') 95 | data_length = len(data) 96 | return int_to_bytes(data_length, 2) + data 97 | 98 | 99 | def encode_data_with_length(data: bytes) -> bytes: 100 | data_length = len(data) 101 | return int_to_bytes(data_length, 2) + data 102 | 103 | 104 | @asyncio.coroutine 105 | def decode_packet_id(reader) -> int: 106 | """ 107 | Read a packet ID as 2-bytes int from stream according to MQTT specification (2.3.1) 108 | :param reader: Stream reader 109 | :return: Packet ID 110 | """ 111 | packet_id_bytes = yield from read_or_raise(reader, 2) 112 | packet_id = unpack("!H", packet_id_bytes) 113 | return packet_id[0] 114 | 115 | 116 | def int_to_bytes_str(value: int) -> bytes: 117 | """ 118 | Converts a int value to a bytes array containing the numeric character. 119 | Ex: 123 -> b'123' 120 | :param value: int value to convert 121 | :return: bytes array 122 | """ 123 | return str(value).encode('utf-8') 124 | -------------------------------------------------------------------------------- /hbmqtt/errors.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2015 Nicolas JOUANIN 2 | # 3 | # See the file license.txt for copying permission. 4 | 5 | 6 | class HBMQTTException(Exception): 7 | """ 8 | HBMQTT base exception 9 | """ 10 | pass 11 | 12 | 13 | class MQTTException(Exception): 14 | """ 15 | Base class for all errors refering to MQTT specifications 16 | """ 17 | pass 18 | 19 | 20 | class CodecException(Exception): 21 | """ 22 | Exceptions thrown by packet encode/decode functions 23 | """ 24 | pass 25 | 26 | 27 | class NoDataException(Exception): 28 | """ 29 | Exceptions thrown by packet encode/decode functions 30 | """ 31 | pass 32 | -------------------------------------------------------------------------------- /hbmqtt/mqtt/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2015 Nicolas JOUANIN 2 | # 3 | # See the file license.txt for copying permission. 4 | from hbmqtt.errors import HBMQTTException 5 | from hbmqtt.mqtt.packet import ( 6 | CONNECT, CONNACK, PUBLISH, PUBACK, PUBREC, PUBREL, PUBCOMP, SUBSCRIBE, 7 | SUBACK, UNSUBSCRIBE, UNSUBACK, PINGREQ, PINGRESP, DISCONNECT, 8 | MQTTFixedHeader) 9 | from hbmqtt.mqtt.connect import ConnectPacket 10 | from hbmqtt.mqtt.connack import ConnackPacket 11 | from hbmqtt.mqtt.disconnect import DisconnectPacket 12 | from hbmqtt.mqtt.pingreq import PingReqPacket 13 | from hbmqtt.mqtt.pingresp import PingRespPacket 14 | from hbmqtt.mqtt.publish import PublishPacket 15 | from hbmqtt.mqtt.puback import PubackPacket 16 | from hbmqtt.mqtt.pubrec import PubrecPacket 17 | from hbmqtt.mqtt.pubrel import PubrelPacket 18 | from hbmqtt.mqtt.pubcomp import PubcompPacket 19 | from hbmqtt.mqtt.subscribe import SubscribePacket 20 | from hbmqtt.mqtt.suback import SubackPacket 21 | from hbmqtt.mqtt.unsubscribe import UnsubscribePacket 22 | from hbmqtt.mqtt.unsuback import UnsubackPacket 23 | 24 | packet_dict = { 25 | CONNECT: ConnectPacket, 26 | CONNACK: ConnackPacket, 27 | PUBLISH: PublishPacket, 28 | PUBACK: PubackPacket, 29 | PUBREC: PubrecPacket, 30 | PUBREL: PubrelPacket, 31 | PUBCOMP: PubcompPacket, 32 | SUBSCRIBE: SubscribePacket, 33 | SUBACK: SubackPacket, 34 | UNSUBSCRIBE: UnsubscribePacket, 35 | UNSUBACK: UnsubackPacket, 36 | PINGREQ: PingReqPacket, 37 | PINGRESP: PingRespPacket, 38 | DISCONNECT: DisconnectPacket 39 | } 40 | 41 | 42 | def packet_class(fixed_header: MQTTFixedHeader): 43 | try: 44 | cls = packet_dict[fixed_header.packet_type] 45 | return cls 46 | except KeyError: 47 | raise HBMQTTException("Unexpected packet Type '%s'" % fixed_header.packet_type) 48 | -------------------------------------------------------------------------------- /hbmqtt/mqtt/connack.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2015 Nicolas JOUANIN 2 | # 3 | # See the file license.txt for copying permission. 4 | import asyncio 5 | from hbmqtt.mqtt.packet import CONNACK, MQTTPacket, MQTTFixedHeader, MQTTVariableHeader 6 | from hbmqtt.codecs import read_or_raise, bytes_to_int 7 | from hbmqtt.errors import HBMQTTException 8 | from hbmqtt.adapters import ReaderAdapter 9 | 10 | CONNECTION_ACCEPTED = 0x00 11 | UNACCEPTABLE_PROTOCOL_VERSION = 0x01 12 | IDENTIFIER_REJECTED = 0x02 13 | SERVER_UNAVAILABLE = 0x03 14 | BAD_USERNAME_PASSWORD = 0x04 15 | NOT_AUTHORIZED = 0x05 16 | 17 | 18 | class ConnackVariableHeader(MQTTVariableHeader): 19 | 20 | __slots__ = ('session_parent', 'return_code') 21 | 22 | def __init__(self, session_parent=None, return_code=None): 23 | super().__init__() 24 | self.session_parent = session_parent 25 | self.return_code = return_code 26 | 27 | @classmethod 28 | @asyncio.coroutine 29 | def from_stream(cls, reader: ReaderAdapter, fixed_header: MQTTFixedHeader): 30 | data = yield from read_or_raise(reader, 2) 31 | session_parent = data[0] & 0x01 32 | return_code = bytes_to_int(data[1]) 33 | return cls(session_parent, return_code) 34 | 35 | def to_bytes(self): 36 | out = bytearray(2) 37 | # Connect acknowledge flags 38 | if self.session_parent: 39 | out[0] = 1 40 | else: 41 | out[0] = 0 42 | # return code 43 | out[1] = self.return_code 44 | 45 | return out 46 | 47 | def __repr__(self): 48 | return type(self).__name__ + '(session_parent={0}, return_code={1})'\ 49 | .format(hex(self.session_parent), hex(self.return_code)) 50 | 51 | 52 | class ConnackPacket(MQTTPacket): 53 | VARIABLE_HEADER = ConnackVariableHeader 54 | PAYLOAD = None 55 | 56 | @property 57 | def return_code(self): 58 | return self.variable_header.return_code 59 | 60 | @return_code.setter 61 | def return_code(self, return_code): 62 | self.variable_header.return_code = return_code 63 | 64 | @property 65 | def session_parent(self): 66 | return self.variable_header.session_parent 67 | 68 | @session_parent.setter 69 | def session_parent(self, session_parent): 70 | self.variable_header.session_parent = session_parent 71 | 72 | def __init__(self, fixed: MQTTFixedHeader=None, variable_header: ConnackVariableHeader=None, payload=None): 73 | if fixed is None: 74 | header = MQTTFixedHeader(CONNACK, 0x00) 75 | else: 76 | if fixed.packet_type is not CONNACK: 77 | raise HBMQTTException("Invalid fixed packet type %s for ConnackPacket init" % fixed.packet_type) 78 | header = fixed 79 | super().__init__(header) 80 | self.variable_header = variable_header 81 | self.payload = None 82 | 83 | @classmethod 84 | def build(cls, session_parent=None, return_code=None): 85 | v_header = ConnackVariableHeader(session_parent, return_code) 86 | packet = ConnackPacket(variable_header=v_header) 87 | return packet 88 | -------------------------------------------------------------------------------- /hbmqtt/mqtt/constants.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2015 Nicolas JOUANIN 2 | # 3 | # See the file license.txt for copying permission. 4 | 5 | QOS_0 = 0x00 6 | QOS_1 = 0x01 7 | QOS_2 = 0x02 8 | -------------------------------------------------------------------------------- /hbmqtt/mqtt/disconnect.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2015 Nicolas JOUANIN 2 | # 3 | # See the file license.txt for copying permission. 4 | from hbmqtt.mqtt.packet import MQTTPacket, MQTTFixedHeader, DISCONNECT 5 | from hbmqtt.errors import HBMQTTException 6 | 7 | 8 | class DisconnectPacket(MQTTPacket): 9 | VARIABLE_HEADER = None 10 | PAYLOAD = None 11 | 12 | def __init__(self, fixed: MQTTFixedHeader=None): 13 | if fixed is None: 14 | header = MQTTFixedHeader(DISCONNECT, 0x00) 15 | else: 16 | if fixed.packet_type is not DISCONNECT: 17 | raise HBMQTTException("Invalid fixed packet type %s for DisconnectPacket init" % fixed.packet_type) 18 | header = fixed 19 | super().__init__(header) 20 | self.variable_header = None 21 | self.payload = None 22 | -------------------------------------------------------------------------------- /hbmqtt/mqtt/packet.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2015 Nicolas JOUANIN 2 | # 3 | # See the file license.txt for copying permission. 4 | import asyncio 5 | 6 | from hbmqtt.codecs import bytes_to_hex_str, decode_packet_id, int_to_bytes, read_or_raise 7 | from hbmqtt.errors import CodecException, MQTTException, NoDataException 8 | from hbmqtt.adapters import ReaderAdapter, WriterAdapter 9 | from datetime import datetime 10 | from struct import unpack 11 | 12 | 13 | RESERVED_0 = 0x00 14 | CONNECT = 0x01 15 | CONNACK = 0x02 16 | PUBLISH = 0x03 17 | PUBACK = 0x04 18 | PUBREC = 0x05 19 | PUBREL = 0x06 20 | PUBCOMP = 0x07 21 | SUBSCRIBE = 0x08 22 | SUBACK = 0x09 23 | UNSUBSCRIBE = 0x0a 24 | UNSUBACK = 0x0b 25 | PINGREQ = 0x0c 26 | PINGRESP = 0x0d 27 | DISCONNECT = 0x0e 28 | RESERVED_15 = 0x0f 29 | 30 | 31 | class MQTTFixedHeader: 32 | 33 | __slots__ = ('packet_type', 'remaining_length', 'flags') 34 | 35 | def __init__(self, packet_type, flags=0, length=0): 36 | self.packet_type = packet_type 37 | self.remaining_length = length 38 | self.flags = flags 39 | 40 | def to_bytes(self): 41 | def encode_remaining_length(length: int): 42 | encoded = bytearray() 43 | while True: 44 | length_byte = length % 0x80 45 | length //= 0x80 46 | if length > 0: 47 | length_byte |= 0x80 48 | encoded.append(length_byte) 49 | if length <= 0: 50 | break 51 | return encoded 52 | 53 | out = bytearray() 54 | packet_type = 0 55 | try: 56 | packet_type = (self.packet_type << 4) | self.flags 57 | out.append(packet_type) 58 | except OverflowError: 59 | raise CodecException('packet_type encoding exceed 1 byte length: value=%d', packet_type) 60 | 61 | encoded_length = encode_remaining_length(self.remaining_length) 62 | out.extend(encoded_length) 63 | 64 | return out 65 | 66 | @asyncio.coroutine 67 | def to_stream(self, writer: WriterAdapter): 68 | writer.write(self.to_bytes()) 69 | 70 | @property 71 | def bytes_length(self): 72 | return len(self.to_bytes()) 73 | 74 | @classmethod 75 | @asyncio.coroutine 76 | def from_stream(cls, reader: ReaderAdapter): 77 | """ 78 | Read and decode MQTT message fixed header from stream 79 | :return: FixedHeader instance 80 | """ 81 | @asyncio.coroutine 82 | def decode_remaining_length(): 83 | """ 84 | Decode message length according to MQTT specifications 85 | :return: 86 | """ 87 | multiplier = 1 88 | value = 0 89 | buffer = bytearray() 90 | while True: 91 | encoded_byte = yield from reader.read(1) 92 | int_byte = unpack('!B', encoded_byte) 93 | buffer.append(int_byte[0]) 94 | value += (int_byte[0] & 0x7f) * multiplier 95 | if (int_byte[0] & 0x80) == 0: 96 | break 97 | else: 98 | multiplier *= 128 99 | if multiplier > 128 * 128 * 128: 100 | raise MQTTException("Invalid remaining length bytes:%s, packet_type=%d" % (bytes_to_hex_str(buffer), msg_type)) 101 | return value 102 | 103 | try: 104 | byte1 = yield from read_or_raise(reader, 1) 105 | int1 = unpack('!B', byte1) 106 | msg_type = (int1[0] & 0xf0) >> 4 107 | flags = int1[0] & 0x0f 108 | remain_length = yield from decode_remaining_length() 109 | 110 | return cls(msg_type, flags, remain_length) 111 | except NoDataException: 112 | return None 113 | 114 | def __repr__(self): 115 | return type(self).__name__ + '(length={0}, flags={1})'.\ 116 | format(self.remaining_length, hex(self.flags)) 117 | 118 | 119 | class MQTTVariableHeader: 120 | def __init__(self): 121 | pass 122 | 123 | @asyncio.coroutine 124 | def to_stream(self, writer: asyncio.StreamWriter): 125 | writer.write(self.to_bytes()) 126 | yield from writer.drain() 127 | 128 | def to_bytes(self) -> bytes: 129 | """ 130 | Serialize header data to a byte array conforming to MQTT protocol 131 | :return: serialized data 132 | """ 133 | 134 | @property 135 | def bytes_length(self): 136 | return len(self.to_bytes()) 137 | 138 | @classmethod 139 | @asyncio.coroutine 140 | def from_stream(cls, reader: asyncio.StreamReader, fixed_header: MQTTFixedHeader): 141 | pass 142 | 143 | 144 | class PacketIdVariableHeader(MQTTVariableHeader): 145 | 146 | __slots__ = ('packet_id',) 147 | 148 | def __init__(self, packet_id): 149 | super().__init__() 150 | self.packet_id = packet_id 151 | 152 | def to_bytes(self): 153 | out = b'' 154 | out += int_to_bytes(self.packet_id, 2) 155 | return out 156 | 157 | @classmethod 158 | @asyncio.coroutine 159 | def from_stream(cls, reader: ReaderAdapter, fixed_header: MQTTFixedHeader): 160 | packet_id = yield from decode_packet_id(reader) 161 | return cls(packet_id) 162 | 163 | def __repr__(self): 164 | return type(self).__name__ + '(packet_id={0})'.format(self.packet_id) 165 | 166 | 167 | class MQTTPayload: 168 | def __init__(self): 169 | pass 170 | 171 | @asyncio.coroutine 172 | def to_stream(self, writer: asyncio.StreamWriter): 173 | writer.write(self.to_bytes()) 174 | yield from writer.drain() 175 | 176 | def to_bytes(self, fixed_header: MQTTFixedHeader, variable_header: MQTTVariableHeader): 177 | pass 178 | 179 | @classmethod 180 | @asyncio.coroutine 181 | def from_stream(cls, reader: asyncio.StreamReader, fixed_header: MQTTFixedHeader, 182 | variable_header: MQTTVariableHeader): 183 | pass 184 | 185 | 186 | class MQTTPacket: 187 | 188 | __slots__ = ('fixed_header', 'variable_header', 'payload', 'protocol_ts') 189 | 190 | FIXED_HEADER = MQTTFixedHeader 191 | VARIABLE_HEADER = None 192 | PAYLOAD = None 193 | 194 | def __init__(self, fixed: MQTTFixedHeader, variable_header: MQTTVariableHeader=None, payload: MQTTPayload=None): 195 | self.fixed_header = fixed 196 | self.variable_header = variable_header 197 | self.payload = payload 198 | self.protocol_ts = None 199 | 200 | @asyncio.coroutine 201 | def to_stream(self, writer: asyncio.StreamWriter): 202 | writer.write(self.to_bytes()) 203 | yield from writer.drain() 204 | self.protocol_ts = datetime.now() 205 | 206 | def to_bytes(self) -> bytes: 207 | if self.variable_header: 208 | variable_header_bytes = self.variable_header.to_bytes() 209 | else: 210 | variable_header_bytes = b'' 211 | if self.payload: 212 | payload_bytes = self.payload.to_bytes(self.fixed_header, self.variable_header) 213 | else: 214 | payload_bytes = b'' 215 | 216 | self.fixed_header.remaining_length = len(variable_header_bytes) + len(payload_bytes) 217 | fixed_header_bytes = self.fixed_header.to_bytes() 218 | 219 | return fixed_header_bytes + variable_header_bytes + payload_bytes 220 | 221 | @classmethod 222 | @asyncio.coroutine 223 | def from_stream(cls, reader: ReaderAdapter, fixed_header=None, variable_header=None): 224 | if fixed_header is None: 225 | fixed_header = yield from cls.FIXED_HEADER.from_stream(reader) 226 | if cls.VARIABLE_HEADER: 227 | if variable_header is None: 228 | variable_header = yield from cls.VARIABLE_HEADER.from_stream(reader, fixed_header) 229 | else: 230 | variable_header = None 231 | if cls.PAYLOAD: 232 | payload = yield from cls.PAYLOAD.from_stream(reader, fixed_header, variable_header) 233 | else: 234 | payload = None 235 | 236 | if fixed_header and not variable_header and not payload: 237 | instance = cls(fixed_header) 238 | elif fixed_header and not payload: 239 | instance = cls(fixed_header, variable_header) 240 | else: 241 | instance = cls(fixed_header, variable_header, payload) 242 | instance.protocol_ts = datetime.now() 243 | return instance 244 | 245 | @property 246 | def bytes_length(self): 247 | return len(self.to_bytes()) 248 | 249 | def __repr__(self): 250 | return type(self).__name__ + '(ts={0!s}, fixed={1!r}, variable={2!r}, payload={3!r})'.\ 251 | format(self.protocol_ts, self.fixed_header, self.variable_header, self.payload) 252 | -------------------------------------------------------------------------------- /hbmqtt/mqtt/pingreq.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2015 Nicolas JOUANIN 2 | # 3 | # See the file license.txt for copying permission. 4 | from hbmqtt.mqtt.packet import MQTTPacket, MQTTFixedHeader, PINGREQ 5 | from hbmqtt.errors import HBMQTTException 6 | 7 | 8 | class PingReqPacket(MQTTPacket): 9 | VARIABLE_HEADER = None 10 | PAYLOAD = None 11 | 12 | def __init__(self, fixed: MQTTFixedHeader=None): 13 | if fixed is None: 14 | header = MQTTFixedHeader(PINGREQ, 0x00) 15 | else: 16 | if fixed.packet_type is not PINGREQ: 17 | raise HBMQTTException("Invalid fixed packet type %s for PingReqPacket init" % fixed.packet_type) 18 | header = fixed 19 | super().__init__(header) 20 | self.variable_header = None 21 | self.payload = None 22 | -------------------------------------------------------------------------------- /hbmqtt/mqtt/pingresp.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2015 Nicolas JOUANIN 2 | # 3 | # See the file license.txt for copying permission. 4 | from hbmqtt.mqtt.packet import MQTTPacket, MQTTFixedHeader, PINGRESP 5 | from hbmqtt.errors import HBMQTTException 6 | 7 | 8 | class PingRespPacket(MQTTPacket): 9 | VARIABLE_HEADER = None 10 | PAYLOAD = None 11 | 12 | def __init__(self, fixed: MQTTFixedHeader=None): 13 | if fixed is None: 14 | header = MQTTFixedHeader(PINGRESP, 0x00) 15 | else: 16 | if fixed.packet_type is not PINGRESP: 17 | raise HBMQTTException("Invalid fixed packet type %s for PingRespPacket init" % fixed.packet_type) 18 | header = fixed 19 | super().__init__(header) 20 | self.variable_header = None 21 | self.payload = None 22 | 23 | @classmethod 24 | def build(cls): 25 | return cls() 26 | -------------------------------------------------------------------------------- /hbmqtt/mqtt/protocol/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/njouanin/hbmqtt/8b3e915091192b112c93ce7101ec848eadf7e383/hbmqtt/mqtt/protocol/__init__.py -------------------------------------------------------------------------------- /hbmqtt/mqtt/protocol/broker_handler.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2015 Nicolas JOUANIN 2 | # 3 | # See the file license.txt for copying permission. 4 | import asyncio 5 | from asyncio import futures, Queue 6 | from hbmqtt.mqtt.protocol.handler import ProtocolHandler 7 | from hbmqtt.mqtt.connack import ( 8 | CONNECTION_ACCEPTED, UNACCEPTABLE_PROTOCOL_VERSION, IDENTIFIER_REJECTED, 9 | BAD_USERNAME_PASSWORD, NOT_AUTHORIZED, ConnackPacket) 10 | from hbmqtt.mqtt.connect import ConnectPacket 11 | from hbmqtt.mqtt.pingreq import PingReqPacket 12 | from hbmqtt.mqtt.pingresp import PingRespPacket 13 | from hbmqtt.mqtt.subscribe import SubscribePacket 14 | from hbmqtt.mqtt.suback import SubackPacket 15 | from hbmqtt.mqtt.unsubscribe import UnsubscribePacket 16 | from hbmqtt.mqtt.unsuback import UnsubackPacket 17 | from hbmqtt.utils import format_client_message 18 | from hbmqtt.session import Session 19 | from hbmqtt.plugins.manager import PluginManager 20 | from hbmqtt.adapters import ReaderAdapter, WriterAdapter 21 | from hbmqtt.errors import MQTTException 22 | from .handler import EVENT_MQTT_PACKET_RECEIVED, EVENT_MQTT_PACKET_SENT 23 | 24 | 25 | class BrokerProtocolHandler(ProtocolHandler): 26 | def __init__(self, plugins_manager: PluginManager, session: Session=None, loop=None): 27 | super().__init__(plugins_manager, session, loop) 28 | self._disconnect_waiter = None 29 | self._pending_subscriptions = Queue(loop=self._loop) 30 | self._pending_unsubscriptions = Queue(loop=self._loop) 31 | 32 | @asyncio.coroutine 33 | def start(self): 34 | yield from super().start() 35 | if self._disconnect_waiter is None: 36 | self._disconnect_waiter = futures.Future(loop=self._loop) 37 | 38 | @asyncio.coroutine 39 | def stop(self): 40 | yield from super().stop() 41 | if self._disconnect_waiter is not None and not self._disconnect_waiter.done(): 42 | self._disconnect_waiter.set_result(None) 43 | 44 | @asyncio.coroutine 45 | def wait_disconnect(self): 46 | return (yield from self._disconnect_waiter) 47 | 48 | def handle_write_timeout(self): 49 | pass 50 | 51 | def handle_read_timeout(self): 52 | if self._disconnect_waiter is not None and not self._disconnect_waiter.done(): 53 | self._disconnect_waiter.set_result(None) 54 | 55 | @asyncio.coroutine 56 | def handle_disconnect(self, disconnect): 57 | self.logger.debug("Client disconnecting") 58 | if self._disconnect_waiter and not self._disconnect_waiter.done(): 59 | self.logger.debug("Setting waiter result to %r" % disconnect) 60 | self._disconnect_waiter.set_result(disconnect) 61 | 62 | @asyncio.coroutine 63 | def handle_connection_closed(self): 64 | yield from self.handle_disconnect(None) 65 | 66 | @asyncio.coroutine 67 | def handle_connect(self, connect: ConnectPacket): 68 | # Broker handler shouldn't received CONNECT message during messages handling 69 | # as CONNECT messages are managed by the broker on client connection 70 | self.logger.error('%s [MQTT-3.1.0-2] %s : CONNECT message received during messages handling' % 71 | (self.session.client_id, format_client_message(self.session))) 72 | if self._disconnect_waiter is not None and not self._disconnect_waiter.done(): 73 | self._disconnect_waiter.set_result(None) 74 | 75 | @asyncio.coroutine 76 | def handle_pingreq(self, pingreq: PingReqPacket): 77 | yield from self._send_packet(PingRespPacket.build()) 78 | 79 | @asyncio.coroutine 80 | def handle_subscribe(self, subscribe: SubscribePacket): 81 | subscription = {'packet_id': subscribe.variable_header.packet_id, 'topics': subscribe.payload.topics} 82 | yield from self._pending_subscriptions.put(subscription) 83 | 84 | @asyncio.coroutine 85 | def handle_unsubscribe(self, unsubscribe: UnsubscribePacket): 86 | unsubscription = {'packet_id': unsubscribe.variable_header.packet_id, 'topics': unsubscribe.payload.topics} 87 | yield from self._pending_unsubscriptions.put(unsubscription) 88 | 89 | @asyncio.coroutine 90 | def get_next_pending_subscription(self): 91 | subscription = yield from self._pending_subscriptions.get() 92 | return subscription 93 | 94 | @asyncio.coroutine 95 | def get_next_pending_unsubscription(self): 96 | unsubscription = yield from self._pending_unsubscriptions.get() 97 | return unsubscription 98 | 99 | @asyncio.coroutine 100 | def mqtt_acknowledge_subscription(self, packet_id, return_codes): 101 | suback = SubackPacket.build(packet_id, return_codes) 102 | yield from self._send_packet(suback) 103 | 104 | @asyncio.coroutine 105 | def mqtt_acknowledge_unsubscription(self, packet_id): 106 | unsuback = UnsubackPacket.build(packet_id) 107 | yield from self._send_packet(unsuback) 108 | 109 | @asyncio.coroutine 110 | def mqtt_connack_authorize(self, authorize: bool): 111 | if authorize: 112 | connack = ConnackPacket.build(self.session.parent, CONNECTION_ACCEPTED) 113 | else: 114 | connack = ConnackPacket.build(self.session.parent, NOT_AUTHORIZED) 115 | yield from self._send_packet(connack) 116 | 117 | @classmethod 118 | @asyncio.coroutine 119 | def init_from_connect(cls, reader: ReaderAdapter, writer: WriterAdapter, plugins_manager, loop=None): 120 | """ 121 | 122 | :param reader: 123 | :param writer: 124 | :param plugins_manager: 125 | :param loop: 126 | :return: 127 | """ 128 | remote_address, remote_port = writer.get_peer_info() 129 | connect = yield from ConnectPacket.from_stream(reader) 130 | yield from plugins_manager.fire_event(EVENT_MQTT_PACKET_RECEIVED, packet=connect) 131 | #this shouldn't be required anymore since broker generates for each client a random client_id if not provided 132 | #[MQTT-3.1.3-6] 133 | if connect.payload.client_id is None: 134 | raise MQTTException('[[MQTT-3.1.3-3]] : Client identifier must be present') 135 | 136 | if connect.variable_header.will_flag: 137 | if connect.payload.will_topic is None or connect.payload.will_message is None: 138 | raise MQTTException('will flag set, but will topic/message not present in payload') 139 | 140 | if connect.variable_header.reserved_flag: 141 | raise MQTTException('[MQTT-3.1.2-3] CONNECT reserved flag must be set to 0') 142 | if connect.proto_name != "MQTT": 143 | raise MQTTException('[MQTT-3.1.2-1] Incorrect protocol name: "%s"' % connect.proto_name) 144 | 145 | connack = None 146 | error_msg = None 147 | if connect.proto_level != 4: 148 | # only MQTT 3.1.1 supported 149 | error_msg = 'Invalid protocol from %s: %d' % ( 150 | format_client_message(address=remote_address, port=remote_port), connect.proto_level) 151 | connack = ConnackPacket.build(0, UNACCEPTABLE_PROTOCOL_VERSION) # [MQTT-3.2.2-4] session_parent=0 152 | elif not connect.username_flag and connect.password_flag: 153 | connack = ConnackPacket.build(0, BAD_USERNAME_PASSWORD) # [MQTT-3.1.2-22] 154 | elif connect.username_flag and not connect.password_flag: 155 | connack = ConnackPacket.build(0, BAD_USERNAME_PASSWORD) # [MQTT-3.1.2-22] 156 | elif connect.username_flag and connect.username is None: 157 | error_msg = 'Invalid username from %s' % ( 158 | format_client_message(address=remote_address, port=remote_port)) 159 | connack = ConnackPacket.build(0, BAD_USERNAME_PASSWORD) # [MQTT-3.2.2-4] session_parent=0 160 | elif connect.password_flag and connect.password is None: 161 | error_msg = 'Invalid password %s' % (format_client_message(address=remote_address, port=remote_port)) 162 | connack = ConnackPacket.build(0, BAD_USERNAME_PASSWORD) # [MQTT-3.2.2-4] session_parent=0 163 | elif connect.clean_session_flag is False and (connect.payload.client_id_is_random): 164 | error_msg = '[MQTT-3.1.3-8] [MQTT-3.1.3-9] %s: No client Id provided (cleansession=0)' % ( 165 | format_client_message(address=remote_address, port=remote_port)) 166 | connack = ConnackPacket.build(0, IDENTIFIER_REJECTED) 167 | if connack is not None: 168 | yield from plugins_manager.fire_event(EVENT_MQTT_PACKET_SENT, packet=connack) 169 | yield from connack.to_stream(writer) 170 | yield from writer.close() 171 | raise MQTTException(error_msg) 172 | 173 | incoming_session = Session(loop) 174 | incoming_session.client_id = connect.client_id 175 | incoming_session.clean_session = connect.clean_session_flag 176 | incoming_session.will_flag = connect.will_flag 177 | incoming_session.will_retain = connect.will_retain_flag 178 | incoming_session.will_qos = connect.will_qos 179 | incoming_session.will_topic = connect.will_topic 180 | incoming_session.will_message = connect.will_message 181 | incoming_session.username = connect.username 182 | incoming_session.password = connect.password 183 | if connect.keep_alive > 0: 184 | incoming_session.keep_alive = connect.keep_alive 185 | else: 186 | incoming_session.keep_alive = 0 187 | 188 | handler = cls(plugins_manager, loop=loop) 189 | return handler, incoming_session 190 | -------------------------------------------------------------------------------- /hbmqtt/mqtt/protocol/client_handler.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2015 Nicolas JOUANIN 2 | # 3 | # See the file license.txt for copying permission. 4 | import asyncio 5 | from asyncio import futures 6 | import sys 7 | from hbmqtt.mqtt.protocol.handler import ProtocolHandler, EVENT_MQTT_PACKET_RECEIVED 8 | from hbmqtt.mqtt.disconnect import DisconnectPacket 9 | from hbmqtt.mqtt.pingreq import PingReqPacket 10 | from hbmqtt.mqtt.pingresp import PingRespPacket 11 | from hbmqtt.mqtt.subscribe import SubscribePacket 12 | from hbmqtt.mqtt.suback import SubackPacket 13 | from hbmqtt.mqtt.unsubscribe import UnsubscribePacket 14 | from hbmqtt.mqtt.unsuback import UnsubackPacket 15 | from hbmqtt.mqtt.connect import ConnectVariableHeader, ConnectPayload, ConnectPacket 16 | from hbmqtt.mqtt.connack import ConnackPacket 17 | from hbmqtt.session import Session 18 | from hbmqtt.plugins.manager import PluginManager 19 | 20 | 21 | class ClientProtocolHandler(ProtocolHandler): 22 | def __init__(self, plugins_manager: PluginManager, session: Session=None, loop=None): 23 | super().__init__(plugins_manager, session, loop=loop) 24 | self._ping_task = None 25 | self._pingresp_queue = asyncio.Queue(loop=self._loop) 26 | self._subscriptions_waiter = dict() 27 | self._unsubscriptions_waiter = dict() 28 | self._disconnect_waiter = None 29 | 30 | @asyncio.coroutine 31 | def start(self): 32 | yield from super().start() 33 | if self._disconnect_waiter is None: 34 | self._disconnect_waiter = futures.Future(loop=self._loop) 35 | 36 | @asyncio.coroutine 37 | def stop(self): 38 | yield from super().stop() 39 | if self._ping_task: 40 | try: 41 | self.logger.debug("Cancel ping task") 42 | self._ping_task.cancel() 43 | except BaseException: 44 | pass 45 | if not self._disconnect_waiter.done(): 46 | self._disconnect_waiter.cancel() 47 | self._disconnect_waiter = None 48 | 49 | def _build_connect_packet(self): 50 | vh = ConnectVariableHeader() 51 | payload = ConnectPayload() 52 | 53 | vh.keep_alive = self.session.keep_alive 54 | vh.clean_session_flag = self.session.clean_session 55 | vh.will_retain_flag = self.session.will_retain 56 | payload.client_id = self.session.client_id 57 | 58 | if self.session.username: 59 | vh.username_flag = True 60 | payload.username = self.session.username 61 | else: 62 | vh.username_flag = False 63 | 64 | if self.session.password: 65 | vh.password_flag = True 66 | payload.password = self.session.password 67 | else: 68 | vh.password_flag = False 69 | if self.session.will_flag: 70 | vh.will_flag = True 71 | vh.will_qos = self.session.will_qos 72 | payload.will_message = self.session.will_message 73 | payload.will_topic = self.session.will_topic 74 | else: 75 | vh.will_flag = False 76 | 77 | packet = ConnectPacket(vh=vh, payload=payload) 78 | return packet 79 | 80 | @asyncio.coroutine 81 | def mqtt_connect(self): 82 | connect_packet = self._build_connect_packet() 83 | yield from self._send_packet(connect_packet) 84 | connack = yield from ConnackPacket.from_stream(self.reader) 85 | yield from self.plugins_manager.fire_event(EVENT_MQTT_PACKET_RECEIVED, packet=connack, session=self.session) 86 | return connack.return_code 87 | 88 | def handle_write_timeout(self): 89 | try: 90 | if not self._ping_task: 91 | self.logger.debug("Scheduling Ping") 92 | self._ping_task = asyncio.ensure_future(self.mqtt_ping()) 93 | except BaseException as be: 94 | self.logger.debug("Exception ignored in ping task: %r" % be) 95 | 96 | def handle_read_timeout(self): 97 | pass 98 | 99 | @asyncio.coroutine 100 | def mqtt_subscribe(self, topics, packet_id): 101 | """ 102 | :param topics: array of topics [{'filter':'/a/b', 'qos': 0x00}, ...] 103 | :return: 104 | """ 105 | 106 | # Build and send SUBSCRIBE message 107 | subscribe = SubscribePacket.build(topics, packet_id) 108 | yield from self._send_packet(subscribe) 109 | 110 | # Wait for SUBACK is received 111 | waiter = futures.Future(loop=self._loop) 112 | self._subscriptions_waiter[subscribe.variable_header.packet_id] = waiter 113 | return_codes = yield from waiter 114 | 115 | del self._subscriptions_waiter[subscribe.variable_header.packet_id] 116 | return return_codes 117 | 118 | @asyncio.coroutine 119 | def handle_suback(self, suback: SubackPacket): 120 | packet_id = suback.variable_header.packet_id 121 | try: 122 | waiter = self._subscriptions_waiter.get(packet_id) 123 | waiter.set_result(suback.payload.return_codes) 124 | except KeyError as ke: 125 | self.logger.warning("Received SUBACK for unknown pending subscription with Id: %s" % packet_id) 126 | 127 | @asyncio.coroutine 128 | def mqtt_unsubscribe(self, topics, packet_id): 129 | """ 130 | 131 | :param topics: array of topics ['/a/b', ...] 132 | :return: 133 | """ 134 | unsubscribe = UnsubscribePacket.build(topics, packet_id) 135 | yield from self._send_packet(unsubscribe) 136 | waiter = futures.Future(loop=self._loop) 137 | self._unsubscriptions_waiter[unsubscribe.variable_header.packet_id] = waiter 138 | yield from waiter 139 | del self._unsubscriptions_waiter[unsubscribe.variable_header.packet_id] 140 | 141 | @asyncio.coroutine 142 | def handle_unsuback(self, unsuback: UnsubackPacket): 143 | packet_id = unsuback.variable_header.packet_id 144 | try: 145 | waiter = self._unsubscriptions_waiter.get(packet_id) 146 | waiter.set_result(None) 147 | except KeyError as ke: 148 | self.logger.warning("Received UNSUBACK for unknown pending subscription with Id: %s" % packet_id) 149 | 150 | @asyncio.coroutine 151 | def mqtt_disconnect(self): 152 | disconnect_packet = DisconnectPacket() 153 | yield from self._send_packet(disconnect_packet) 154 | 155 | @asyncio.coroutine 156 | def mqtt_ping(self): 157 | ping_packet = PingReqPacket() 158 | yield from self._send_packet(ping_packet) 159 | resp = yield from self._pingresp_queue.get() 160 | if self._ping_task: 161 | self._ping_task = None 162 | return resp 163 | 164 | @asyncio.coroutine 165 | def handle_pingresp(self, pingresp: PingRespPacket): 166 | yield from self._pingresp_queue.put(pingresp) 167 | 168 | @asyncio.coroutine 169 | def handle_connection_closed(self): 170 | self.logger.debug("Broker closed connection") 171 | if not self._disconnect_waiter.done(): 172 | self._disconnect_waiter.set_result(None) 173 | 174 | @asyncio.coroutine 175 | def wait_disconnect(self): 176 | yield from self._disconnect_waiter 177 | -------------------------------------------------------------------------------- /hbmqtt/mqtt/puback.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2015 Nicolas JOUANIN 2 | # 3 | # See the file license.txt for copying permission. 4 | from hbmqtt.mqtt.packet import MQTTPacket, MQTTFixedHeader, PUBACK, PacketIdVariableHeader 5 | from hbmqtt.errors import HBMQTTException 6 | 7 | 8 | class PubackPacket(MQTTPacket): 9 | VARIABLE_HEADER = PacketIdVariableHeader 10 | PAYLOAD = None 11 | 12 | @property 13 | def packet_id(self): 14 | return self.variable_header.packet_id 15 | 16 | @packet_id.setter 17 | def packet_id(self, val: int): 18 | self.variable_header.packet_id = val 19 | 20 | def __init__(self, fixed: MQTTFixedHeader=None, variable_header: PacketIdVariableHeader=None): 21 | if fixed is None: 22 | header = MQTTFixedHeader(PUBACK, 0x00) 23 | else: 24 | if fixed.packet_type is not PUBACK: 25 | raise HBMQTTException("Invalid fixed packet type %s for PubackPacket init" % fixed.packet_type) 26 | header = fixed 27 | super().__init__(header) 28 | self.variable_header = variable_header 29 | self.payload = None 30 | 31 | @classmethod 32 | def build(cls, packet_id: int): 33 | v_header = PacketIdVariableHeader(packet_id) 34 | packet = PubackPacket(variable_header=v_header) 35 | return packet 36 | -------------------------------------------------------------------------------- /hbmqtt/mqtt/pubcomp.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2015 Nicolas JOUANIN 2 | # 3 | # See the file license.txt for copying permission. 4 | from hbmqtt.mqtt.packet import MQTTPacket, MQTTFixedHeader, PUBCOMP, PacketIdVariableHeader 5 | from hbmqtt.errors import HBMQTTException 6 | 7 | 8 | class PubcompPacket(MQTTPacket): 9 | VARIABLE_HEADER = PacketIdVariableHeader 10 | PAYLOAD = None 11 | 12 | @property 13 | def packet_id(self): 14 | return self.variable_header.packet_id 15 | 16 | @packet_id.setter 17 | def packet_id(self, val: int): 18 | self.variable_header.packet_id = val 19 | 20 | def __init__(self, fixed: MQTTFixedHeader=None, variable_header: PacketIdVariableHeader=None): 21 | if fixed is None: 22 | header = MQTTFixedHeader(PUBCOMP, 0x00) 23 | else: 24 | if fixed.packet_type is not PUBCOMP: 25 | raise HBMQTTException("Invalid fixed packet type %s for PubcompPacket init" % fixed.packet_type) 26 | header = fixed 27 | super().__init__(header) 28 | self.variable_header = variable_header 29 | self.payload = None 30 | 31 | @classmethod 32 | def build(cls, packet_id: int): 33 | v_header = PacketIdVariableHeader(packet_id) 34 | packet = PubcompPacket(variable_header=v_header) 35 | return packet 36 | -------------------------------------------------------------------------------- /hbmqtt/mqtt/publish.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2015 Nicolas JOUANIN 2 | # 3 | # See the file license.txt for copying permission. 4 | import asyncio 5 | 6 | from hbmqtt.mqtt.packet import MQTTPacket, MQTTFixedHeader, PUBLISH, MQTTVariableHeader, MQTTPayload 7 | from hbmqtt.errors import HBMQTTException, MQTTException 8 | from hbmqtt.codecs import decode_packet_id, decode_string, encode_string, int_to_bytes 9 | 10 | 11 | class PublishVariableHeader(MQTTVariableHeader): 12 | 13 | __slots__ = ('topic_name', 'packet_id') 14 | 15 | def __init__(self, topic_name: str, packet_id: int=None): 16 | super().__init__() 17 | if '*' in topic_name: 18 | raise MQTTException("[MQTT-3.3.2-2] Topic name in the PUBLISH Packet MUST NOT contain wildcard characters.") 19 | self.topic_name = topic_name 20 | self.packet_id = packet_id 21 | 22 | def __repr__(self): 23 | return type(self).__name__ + '(topic={0}, packet_id={1})'.format(self.topic_name, self.packet_id) 24 | 25 | def to_bytes(self): 26 | out = bytearray() 27 | out.extend(encode_string(self.topic_name)) 28 | if self.packet_id is not None: 29 | out.extend(int_to_bytes(self.packet_id, 2)) 30 | return out 31 | 32 | @classmethod 33 | @asyncio.coroutine 34 | def from_stream(cls, reader: asyncio.StreamReader, fixed_header: MQTTFixedHeader): 35 | topic_name = yield from decode_string(reader) 36 | has_qos = (fixed_header.flags >> 1) & 0x03 37 | if has_qos: 38 | packet_id = yield from decode_packet_id(reader) 39 | else: 40 | packet_id = None 41 | return cls(topic_name, packet_id) 42 | 43 | 44 | class PublishPayload(MQTTPayload): 45 | 46 | __slots__ = ('data',) 47 | 48 | def __init__(self, data: bytes=None): 49 | super().__init__() 50 | self.data = data 51 | 52 | def to_bytes(self, fixed_header: MQTTFixedHeader, variable_header: MQTTVariableHeader): 53 | return self.data 54 | 55 | @classmethod 56 | @asyncio.coroutine 57 | def from_stream(cls, reader: asyncio.StreamReader, fixed_header: MQTTFixedHeader, 58 | variable_header: MQTTVariableHeader): 59 | data = bytearray() 60 | data_length = fixed_header.remaining_length - variable_header.bytes_length 61 | length_read = 0 62 | while length_read < data_length: 63 | buffer = yield from reader.read(data_length - length_read) 64 | data.extend(buffer) 65 | length_read = len(data) 66 | return cls(data) 67 | 68 | def __repr__(self): 69 | return type(self).__name__ + '(data={0!r})'.format(repr(self.data)) 70 | 71 | 72 | class PublishPacket(MQTTPacket): 73 | VARIABLE_HEADER = PublishVariableHeader 74 | PAYLOAD = PublishPayload 75 | 76 | DUP_FLAG = 0x08 77 | RETAIN_FLAG = 0x01 78 | QOS_FLAG = 0x06 79 | 80 | def __init__(self, fixed: MQTTFixedHeader=None, variable_header: PublishVariableHeader=None, payload=None): 81 | if fixed is None: 82 | header = MQTTFixedHeader(PUBLISH, 0x00) 83 | else: 84 | if fixed.packet_type is not PUBLISH: 85 | raise HBMQTTException("Invalid fixed packet type %s for PublishPacket init" % fixed.packet_type) 86 | header = fixed 87 | 88 | super().__init__(header) 89 | self.variable_header = variable_header 90 | self.payload = payload 91 | 92 | def set_flags(self, dup_flag=False, qos=0, retain_flag=False): 93 | self.dup_flag = dup_flag 94 | self.retain_flag = retain_flag 95 | self.qos = qos 96 | 97 | def _set_header_flag(self, val, mask): 98 | if val: 99 | self.fixed_header.flags |= mask 100 | else: 101 | self.fixed_header.flags &= ~mask 102 | 103 | def _get_header_flag(self, mask): 104 | if self.fixed_header.flags & mask: 105 | return True 106 | else: 107 | return False 108 | 109 | @property 110 | def dup_flag(self) -> bool: 111 | return self._get_header_flag(self.DUP_FLAG) 112 | 113 | @dup_flag.setter 114 | def dup_flag(self, val: bool): 115 | self._set_header_flag(val, self.DUP_FLAG) 116 | 117 | @property 118 | def retain_flag(self) -> bool: 119 | return self._get_header_flag(self.RETAIN_FLAG) 120 | 121 | @retain_flag.setter 122 | def retain_flag(self, val: bool): 123 | self._set_header_flag(val, self.RETAIN_FLAG) 124 | 125 | @property 126 | def qos(self): 127 | return (self.fixed_header.flags & self.QOS_FLAG) >> 1 128 | 129 | @qos.setter 130 | def qos(self, val: int): 131 | self.fixed_header.flags &= 0xf9 132 | self.fixed_header.flags |= (val << 1) 133 | 134 | @property 135 | def packet_id(self): 136 | return self.variable_header.packet_id 137 | 138 | @packet_id.setter 139 | def packet_id(self, val: int): 140 | self.variable_header.packet_id = val 141 | 142 | @property 143 | def data(self): 144 | return self.payload.data 145 | 146 | @data.setter 147 | def data(self, data: bytes): 148 | self.payload.data = data 149 | 150 | @property 151 | def topic_name(self): 152 | return self.variable_header.topic_name 153 | 154 | @topic_name.setter 155 | def topic_name(self, name: str): 156 | self.variable_header.topic_name = name 157 | 158 | @classmethod 159 | def build(cls, topic_name: str, message: bytes, packet_id: int, dup_flag, qos, retain): 160 | v_header = PublishVariableHeader(topic_name, packet_id) 161 | payload = PublishPayload(message) 162 | packet = PublishPacket(variable_header=v_header, payload=payload) 163 | packet.dup_flag = dup_flag 164 | packet.retain_flag = retain 165 | packet.qos = qos 166 | return packet 167 | -------------------------------------------------------------------------------- /hbmqtt/mqtt/pubrec.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2015 Nicolas JOUANIN 2 | # 3 | # See the file license.txt for copying permission. 4 | from hbmqtt.mqtt.packet import MQTTPacket, MQTTFixedHeader, PUBREC, PacketIdVariableHeader 5 | from hbmqtt.errors import HBMQTTException 6 | 7 | 8 | class PubrecPacket(MQTTPacket): 9 | VARIABLE_HEADER = PacketIdVariableHeader 10 | PAYLOAD = None 11 | 12 | @property 13 | def packet_id(self): 14 | return self.variable_header.packet_id 15 | 16 | @packet_id.setter 17 | def packet_id(self, val: int): 18 | self.variable_header.packet_id = val 19 | 20 | def __init__(self, fixed: MQTTFixedHeader=None, variable_header: PacketIdVariableHeader=None): 21 | if fixed is None: 22 | header = MQTTFixedHeader(PUBREC, 0x00) 23 | else: 24 | if fixed.packet_type is not PUBREC: 25 | raise HBMQTTException("Invalid fixed packet type %s for PubrecPacket init" % fixed.packet_type) 26 | header = fixed 27 | super().__init__(header) 28 | self.variable_header = variable_header 29 | self.payload = None 30 | 31 | @classmethod 32 | def build(cls, packet_id: int): 33 | v_header = PacketIdVariableHeader(packet_id) 34 | packet = PubrecPacket(variable_header=v_header) 35 | return packet 36 | -------------------------------------------------------------------------------- /hbmqtt/mqtt/pubrel.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2015 Nicolas JOUANIN 2 | # 3 | # See the file license.txt for copying permission. 4 | from hbmqtt.mqtt.packet import MQTTPacket, MQTTFixedHeader, PUBREL, PacketIdVariableHeader 5 | from hbmqtt.errors import HBMQTTException 6 | 7 | 8 | class PubrelPacket(MQTTPacket): 9 | VARIABLE_HEADER = PacketIdVariableHeader 10 | PAYLOAD = None 11 | 12 | @property 13 | def packet_id(self): 14 | return self.variable_header.packet_id 15 | 16 | @packet_id.setter 17 | def packet_id(self, val: int): 18 | self.variable_header.packet_id = val 19 | 20 | def __init__(self, fixed: MQTTFixedHeader=None, variable_header: PacketIdVariableHeader=None): 21 | if fixed is None: 22 | header = MQTTFixedHeader(PUBREL, 0x02) # [MQTT-3.6.1-1] 23 | else: 24 | if fixed.packet_type is not PUBREL: 25 | raise HBMQTTException("Invalid fixed packet type %s for PubrelPacket init" % fixed.packet_type) 26 | header = fixed 27 | super().__init__(header) 28 | self.variable_header = variable_header 29 | self.payload = None 30 | 31 | @classmethod 32 | def build(cls, packet_id): 33 | variable_header = PacketIdVariableHeader(packet_id) 34 | return PubrelPacket(variable_header=variable_header) 35 | -------------------------------------------------------------------------------- /hbmqtt/mqtt/suback.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2015 Nicolas JOUANIN 2 | # 3 | # See the file license.txt for copying permission. 4 | import asyncio 5 | 6 | from hbmqtt.mqtt.packet import MQTTPacket, MQTTFixedHeader, SUBACK, PacketIdVariableHeader, MQTTPayload, MQTTVariableHeader 7 | from hbmqtt.errors import HBMQTTException, NoDataException 8 | from hbmqtt.adapters import ReaderAdapter 9 | from hbmqtt.codecs import bytes_to_int, int_to_bytes, read_or_raise 10 | 11 | 12 | class SubackPayload(MQTTPayload): 13 | 14 | __slots__ = ('return_codes',) 15 | 16 | RETURN_CODE_00 = 0x00 17 | RETURN_CODE_01 = 0x01 18 | RETURN_CODE_02 = 0x02 19 | RETURN_CODE_80 = 0x80 20 | 21 | def __init__(self, return_codes=[]): 22 | super().__init__() 23 | self.return_codes = return_codes 24 | 25 | def __repr__(self): 26 | return type(self).__name__ + '(return_codes={0})'.format(repr(self.return_codes)) 27 | 28 | def to_bytes(self, fixed_header: MQTTFixedHeader, variable_header: MQTTVariableHeader): 29 | out = b'' 30 | for return_code in self.return_codes: 31 | out += int_to_bytes(return_code, 1) 32 | return out 33 | 34 | @classmethod 35 | @asyncio.coroutine 36 | def from_stream(cls, reader: ReaderAdapter, fixed_header: MQTTFixedHeader, 37 | variable_header: MQTTVariableHeader): 38 | return_codes = [] 39 | bytes_to_read = fixed_header.remaining_length - variable_header.bytes_length 40 | for i in range(0, bytes_to_read): 41 | try: 42 | return_code_byte = yield from read_or_raise(reader, 1) 43 | return_code = bytes_to_int(return_code_byte) 44 | return_codes.append(return_code) 45 | except NoDataException: 46 | break 47 | return cls(return_codes) 48 | 49 | 50 | class SubackPacket(MQTTPacket): 51 | VARIABLE_HEADER = PacketIdVariableHeader 52 | PAYLOAD = SubackPayload 53 | 54 | def __init__(self, fixed: MQTTFixedHeader=None, variable_header: PacketIdVariableHeader=None, payload=None): 55 | if fixed is None: 56 | header = MQTTFixedHeader(SUBACK, 0x00) 57 | else: 58 | if fixed.packet_type is not SUBACK: 59 | raise HBMQTTException("Invalid fixed packet type %s for SubackPacket init" % fixed.packet_type) 60 | header = fixed 61 | 62 | super().__init__(header) 63 | self.variable_header = variable_header 64 | self.payload = payload 65 | 66 | @classmethod 67 | def build(cls, packet_id, return_codes): 68 | variable_header = cls.VARIABLE_HEADER(packet_id) 69 | payload = cls.PAYLOAD(return_codes) 70 | return cls(variable_header=variable_header, payload=payload) 71 | -------------------------------------------------------------------------------- /hbmqtt/mqtt/subscribe.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2015 Nicolas JOUANIN 2 | # 3 | # See the file license.txt for copying permission. 4 | import asyncio 5 | 6 | from hbmqtt.mqtt.packet import MQTTPacket, MQTTFixedHeader, SUBSCRIBE, PacketIdVariableHeader, MQTTPayload, MQTTVariableHeader 7 | from hbmqtt.errors import HBMQTTException, NoDataException 8 | from hbmqtt.codecs import bytes_to_int, decode_string, encode_string, int_to_bytes, read_or_raise 9 | 10 | 11 | class SubscribePayload(MQTTPayload): 12 | 13 | __slots__ = ('topics',) 14 | 15 | def __init__(self, topics=[]): 16 | super().__init__() 17 | self.topics = topics 18 | 19 | def to_bytes(self, fixed_header: MQTTFixedHeader, variable_header: MQTTVariableHeader): 20 | out = b'' 21 | for topic in self.topics: 22 | out += encode_string(topic[0]) 23 | out += int_to_bytes(topic[1], 1) 24 | return out 25 | 26 | @classmethod 27 | @asyncio.coroutine 28 | def from_stream(cls, reader: asyncio.StreamReader, fixed_header: MQTTFixedHeader, 29 | variable_header: MQTTVariableHeader): 30 | topics = [] 31 | payload_length = fixed_header.remaining_length - variable_header.bytes_length 32 | read_bytes = 0 33 | while read_bytes < payload_length: 34 | try: 35 | topic = yield from decode_string(reader) 36 | qos_byte = yield from read_or_raise(reader, 1) 37 | qos = bytes_to_int(qos_byte) 38 | topics.append((topic, qos)) 39 | read_bytes += 2 + len(topic.encode('utf-8')) + 1 40 | except NoDataException as exc: 41 | break 42 | return cls(topics) 43 | 44 | def __repr__(self): 45 | return type(self).__name__ + '(topics={0!r})'.format(self.topics) 46 | 47 | 48 | class SubscribePacket(MQTTPacket): 49 | VARIABLE_HEADER = PacketIdVariableHeader 50 | PAYLOAD = SubscribePayload 51 | 52 | def __init__(self, fixed: MQTTFixedHeader=None, variable_header: PacketIdVariableHeader=None, payload=None): 53 | if fixed is None: 54 | header = MQTTFixedHeader(SUBSCRIBE, 0x02) # [MQTT-3.8.1-1] 55 | else: 56 | if fixed.packet_type is not SUBSCRIBE: 57 | raise HBMQTTException("Invalid fixed packet type %s for SubscribePacket init" % fixed.packet_type) 58 | header = fixed 59 | 60 | super().__init__(header) 61 | self.variable_header = variable_header 62 | self.payload = payload 63 | 64 | @classmethod 65 | def build(cls, topics, packet_id): 66 | v_header = PacketIdVariableHeader(packet_id) 67 | payload = SubscribePayload(topics) 68 | return SubscribePacket(variable_header=v_header, payload=payload) 69 | -------------------------------------------------------------------------------- /hbmqtt/mqtt/unsuback.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2015 Nicolas JOUANIN 2 | # 3 | # See the file license.txt for copying permission. 4 | from hbmqtt.mqtt.packet import MQTTPacket, MQTTFixedHeader, UNSUBACK, PacketIdVariableHeader 5 | from hbmqtt.errors import HBMQTTException 6 | 7 | 8 | class UnsubackPacket(MQTTPacket): 9 | VARIABLE_HEADER = PacketIdVariableHeader 10 | PAYLOAD = None 11 | 12 | def __init__(self, fixed: MQTTFixedHeader=None, variable_header: PacketIdVariableHeader=None, payload=None): 13 | if fixed is None: 14 | header = MQTTFixedHeader(UNSUBACK, 0x00) 15 | else: 16 | if fixed.packet_type is not UNSUBACK: 17 | raise HBMQTTException("Invalid fixed packet type %s for UnsubackPacket init" % fixed.packet_type) 18 | header = fixed 19 | 20 | super().__init__(header) 21 | self.variable_header = variable_header 22 | self.payload = payload 23 | 24 | @classmethod 25 | def build(cls, packet_id): 26 | variable_header = PacketIdVariableHeader(packet_id) 27 | return cls(variable_header=variable_header) 28 | -------------------------------------------------------------------------------- /hbmqtt/mqtt/unsubscribe.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2015 Nicolas JOUANIN 2 | # 3 | # See the file license.txt for copying permission. 4 | import asyncio 5 | 6 | from hbmqtt.mqtt.packet import MQTTPacket, MQTTFixedHeader, UNSUBSCRIBE, PacketIdVariableHeader, MQTTPayload, MQTTVariableHeader 7 | from hbmqtt.errors import HBMQTTException, NoDataException 8 | from hbmqtt.codecs import decode_string, encode_string 9 | 10 | 11 | class UnubscribePayload(MQTTPayload): 12 | 13 | __slots__ = ('topics',) 14 | 15 | def __init__(self, topics=[]): 16 | super().__init__() 17 | self.topics = topics 18 | 19 | def to_bytes(self, fixed_header: MQTTFixedHeader, variable_header: MQTTVariableHeader): 20 | out = b'' 21 | for topic in self.topics: 22 | out += encode_string(topic) 23 | return out 24 | 25 | @classmethod 26 | @asyncio.coroutine 27 | def from_stream(cls, reader: asyncio.StreamReader, fixed_header: MQTTFixedHeader, 28 | variable_header: MQTTVariableHeader): 29 | topics = [] 30 | payload_length = fixed_header.remaining_length - variable_header.bytes_length 31 | read_bytes = 0 32 | while read_bytes < payload_length: 33 | try: 34 | topic = yield from decode_string(reader) 35 | topics.append(topic) 36 | read_bytes += 2 + len(topic.encode('utf-8')) 37 | except NoDataException: 38 | break 39 | return cls(topics) 40 | 41 | 42 | class UnsubscribePacket(MQTTPacket): 43 | VARIABLE_HEADER = PacketIdVariableHeader 44 | PAYLOAD = UnubscribePayload 45 | 46 | def __init__(self, fixed: MQTTFixedHeader=None, variable_header: PacketIdVariableHeader=None, payload=None): 47 | if fixed is None: 48 | header = MQTTFixedHeader(UNSUBSCRIBE, 0x02) # [MQTT-3.10.1-1] 49 | else: 50 | if fixed.packet_type is not UNSUBSCRIBE: 51 | raise HBMQTTException("Invalid fixed packet type %s for UnsubscribePacket init" % fixed.packet_type) 52 | header = fixed 53 | 54 | super().__init__(header) 55 | self.variable_header = variable_header 56 | self.payload = payload 57 | 58 | @classmethod 59 | def build(cls, topics, packet_id): 60 | v_header = PacketIdVariableHeader(packet_id) 61 | payload = UnubscribePayload(topics) 62 | return UnsubscribePacket(variable_header=v_header, payload=payload) 63 | -------------------------------------------------------------------------------- /hbmqtt/plugins/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2015 Nicolas JOUANIN 2 | # 3 | # See the file license.txt for copying permission. 4 | -------------------------------------------------------------------------------- /hbmqtt/plugins/authentication.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2015 Nicolas JOUANIN 2 | # 3 | # See the file license.txt for copying permission. 4 | import logging 5 | import asyncio 6 | from passlib.apps import custom_app_context as pwd_context 7 | 8 | 9 | class BaseAuthPlugin: 10 | def __init__(self, context): 11 | self.context = context 12 | try: 13 | self.auth_config = self.context.config['auth'] 14 | except KeyError: 15 | self.context.logger.warning("'auth' section not found in context configuration") 16 | 17 | def authenticate(self, *args, **kwargs): 18 | if not self.auth_config: 19 | # auth config section not found 20 | self.context.logger.warning("'auth' section not found in context configuration") 21 | return False 22 | return True 23 | 24 | 25 | class AnonymousAuthPlugin(BaseAuthPlugin): 26 | def __init__(self, context): 27 | super().__init__(context) 28 | 29 | @asyncio.coroutine 30 | def authenticate(self, *args, **kwargs): 31 | authenticated = super().authenticate(*args, **kwargs) 32 | if authenticated: 33 | allow_anonymous = self.auth_config.get('allow-anonymous', True) # allow anonymous by default 34 | if allow_anonymous: 35 | authenticated = True 36 | self.context.logger.debug("Authentication success: config allows anonymous") 37 | else: 38 | try: 39 | session = kwargs.get('session', None) 40 | authenticated = True if session.username else False 41 | if self.context.logger.isEnabledFor(logging.DEBUG): 42 | if authenticated: 43 | self.context.logger.debug("Authentication success: session has a non empty username") 44 | else: 45 | self.context.logger.debug("Authentication failure: session has an empty username") 46 | except KeyError: 47 | self.context.logger.warning("Session informations not available") 48 | authenticated = False 49 | return authenticated 50 | 51 | 52 | class FileAuthPlugin(BaseAuthPlugin): 53 | def __init__(self, context): 54 | super().__init__(context) 55 | self._users = dict() 56 | self._read_password_file() 57 | 58 | def _read_password_file(self): 59 | password_file = self.auth_config.get('password-file', None) 60 | if password_file: 61 | try: 62 | with open(password_file) as f: 63 | self.context.logger.debug("Reading user database from %s" % password_file) 64 | for l in f: 65 | line = l.strip() 66 | if not line.startswith('#'): # Allow comments in files 67 | (username, pwd_hash) = line.split(sep=":", maxsplit=3) 68 | if username: 69 | self._users[username] = pwd_hash 70 | self.context.logger.debug("user %s , hash=%s" % (username, pwd_hash)) 71 | self.context.logger.debug("%d user(s) read from file %s" % (len(self._users), password_file)) 72 | except FileNotFoundError: 73 | self.context.logger.warning("Password file %s not found" % password_file) 74 | else: 75 | self.context.logger.debug("Configuration parameter 'password_file' not found") 76 | 77 | @asyncio.coroutine 78 | def authenticate(self, *args, **kwargs): 79 | authenticated = super().authenticate(*args, **kwargs) 80 | if authenticated: 81 | session = kwargs.get('session', None) 82 | if session.username: 83 | hash = self._users.get(session.username, None) 84 | if not hash: 85 | authenticated = False 86 | self.context.logger.debug("No hash found for user '%s'" % session.username) 87 | else: 88 | authenticated = pwd_context.verify(session.password, hash) 89 | else: 90 | return None 91 | return authenticated 92 | -------------------------------------------------------------------------------- /hbmqtt/plugins/logging.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2015 Nicolas JOUANIN 2 | # 3 | # See the file license.txt for copying permission. 4 | 5 | 6 | import logging 7 | import asyncio 8 | from functools import partial 9 | 10 | 11 | class EventLoggerPlugin: 12 | def __init__(self, context): 13 | self.context = context 14 | 15 | @asyncio.coroutine 16 | def log_event(self, *args, **kwargs): 17 | self.context.logger.info("### '%s' EVENT FIRED ###" % kwargs['event_name'].replace('old', '')) 18 | 19 | def __getattr__(self, name): 20 | if name.startswith("on_"): 21 | return partial(self.log_event, event_name=name) 22 | 23 | 24 | class PacketLoggerPlugin: 25 | def __init__(self, context): 26 | self.context = context 27 | 28 | @asyncio.coroutine 29 | def on_mqtt_packet_received(self, *args, **kwargs): 30 | packet = kwargs.get('packet') 31 | session = kwargs.get('session', None) 32 | if self.context.logger.isEnabledFor(logging.DEBUG): 33 | if session: 34 | self.context.logger.debug("%s <-in-- %s" % (session.client_id, repr(packet))) 35 | else: 36 | self.context.logger.debug("<-in-- %s" % repr(packet)) 37 | 38 | @asyncio.coroutine 39 | def on_mqtt_packet_sent(self, *args, **kwargs): 40 | packet = kwargs.get('packet') 41 | session = kwargs.get('session', None) 42 | if self.context.logger.isEnabledFor(logging.DEBUG): 43 | if session: 44 | self.context.logger.debug("%s -out-> %s" % (session.client_id, repr(packet))) 45 | else: 46 | self.context.logger.debug("-out-> %s" % repr(packet)) 47 | -------------------------------------------------------------------------------- /hbmqtt/plugins/manager.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2015 Nicolas JOUANIN 2 | # 3 | # See the file license.txt for copying permission. 4 | 5 | __all__ = ['get_plugin_manager', 'BaseContext', 'PluginManager'] 6 | 7 | import pkg_resources 8 | import logging 9 | import asyncio 10 | import copy 11 | import sys 12 | 13 | from collections import namedtuple 14 | 15 | 16 | Plugin = namedtuple('Plugin', ['name', 'ep', 'object']) 17 | 18 | plugins_manager = dict() 19 | 20 | 21 | def get_plugin_manager(namespace): 22 | global plugins_manager 23 | return plugins_manager.get(namespace, None) 24 | 25 | 26 | class BaseContext: 27 | def __init__(self): 28 | self.loop = None 29 | self.logger = None 30 | 31 | 32 | class PluginManager: 33 | """ 34 | Wraps setuptools Entry point mechanism to provide a basic plugin system. 35 | Plugins are loaded for a given namespace (group). 36 | This plugin manager uses coroutines to run plugin call asynchronously in an event queue 37 | """ 38 | def __init__(self, namespace, context, loop=None): 39 | global plugins_manager 40 | if loop is not None: 41 | self._loop = loop 42 | else: 43 | self._loop = asyncio.get_event_loop() 44 | 45 | self.logger = logging.getLogger(namespace) 46 | if context is None: 47 | self.context = BaseContext() 48 | else: 49 | self.context = context 50 | self.context.loop = self._loop 51 | self._plugins = [] 52 | self._load_plugins(namespace) 53 | self._fired_events = [] 54 | plugins_manager[namespace] = self 55 | 56 | @property 57 | def app_context(self): 58 | return self.context 59 | 60 | def _load_plugins(self, namespace): 61 | self.logger.debug("Loading plugins for namespace %s" % namespace) 62 | for ep in pkg_resources.iter_entry_points(group=namespace): 63 | plugin = self._load_plugin(ep) 64 | self._plugins.append(plugin) 65 | self.logger.debug(" Plugin %s ready" % plugin.ep.name) 66 | 67 | def _load_plugin(self, ep: pkg_resources.EntryPoint): 68 | try: 69 | self.logger.debug(" Loading plugin %s" % ep) 70 | plugin = ep.load(require=True) 71 | self.logger.debug(" Initializing plugin %s" % ep) 72 | plugin_context = copy.copy(self.app_context) 73 | plugin_context.logger = self.logger.getChild(ep.name) 74 | obj = plugin(plugin_context) 75 | return Plugin(ep.name, ep, obj) 76 | except ImportError as ie: 77 | self.logger.warning("Plugin %r import failed: %s" % (ep, ie)) 78 | except pkg_resources.UnknownExtra as ue: 79 | self.logger.warning("Plugin %r dependencies resolution failed: %s" % (ep, ue)) 80 | 81 | def get_plugin(self, name): 82 | """ 83 | Get a plugin by its name from the plugins loaded for the current namespace 84 | :param name: 85 | :return: 86 | """ 87 | for p in self._plugins: 88 | if p.name == name: 89 | return p 90 | return None 91 | 92 | @asyncio.coroutine 93 | def close(self): 94 | """ 95 | Free PluginManager resources and cancel pending event methods 96 | This method call a close() coroutine for each plugin, allowing plugins to close and free resources 97 | :return: 98 | """ 99 | yield from self.map_plugin_coro("close") 100 | for task in self._fired_events: 101 | task.cancel() 102 | 103 | @property 104 | def plugins(self): 105 | """ 106 | Get the loaded plugins list 107 | :return: 108 | """ 109 | return self._plugins 110 | 111 | def _schedule_coro(self, coro): 112 | return asyncio.ensure_future(coro, loop=self._loop) 113 | 114 | @asyncio.coroutine 115 | def fire_event(self, event_name, wait=False, *args, **kwargs): 116 | """ 117 | Fire an event to plugins. 118 | PluginManager schedule @asyncio.coroutinecalls for each plugin on method called "on_" + event_name 119 | For example, on_connect will be called on event 'connect' 120 | Method calls are schedule in the asyn loop. wait parameter must be set to true to wait until all 121 | mehtods are completed. 122 | :param event_name: 123 | :param args: 124 | :param kwargs: 125 | :param wait: indicates if fire_event should wait for plugin calls completion (True), or not 126 | :return: 127 | """ 128 | tasks = [] 129 | event_method_name = "on_" + event_name 130 | for plugin in self._plugins: 131 | event_method = getattr(plugin.object, event_method_name, None) 132 | if event_method: 133 | try: 134 | task = self._schedule_coro(event_method(*args, **kwargs)) 135 | tasks.append(task) 136 | 137 | def clean_fired_events(future): 138 | try: 139 | self._fired_events.remove(future) 140 | except (KeyError, ValueError): 141 | pass 142 | 143 | task.add_done_callback(clean_fired_events) 144 | except AssertionError: 145 | self.logger.error("Method '%s' on plugin '%s' is not a coroutine" % 146 | (event_method_name, plugin.name)) 147 | 148 | self._fired_events.extend(tasks) 149 | if wait: 150 | if tasks: 151 | yield from asyncio.wait(tasks, loop=self._loop) 152 | if self.logger.isEnabledFor(logging.DEBUG): 153 | self.logger.debug("Plugins len(_fired_events)=%d" % (len(self._fired_events))) 154 | 155 | @asyncio.coroutine 156 | def map(self, coro, *args, **kwargs): 157 | """ 158 | Schedule a given coroutine call for each plugin. 159 | The coro called get the Plugin instance as first argument of its method call 160 | :param coro: coro to call on each plugin 161 | :param filter_plugins: list of plugin names to filter (only plugin whose name is in filter are called). 162 | None will call all plugins. [] will call None. 163 | :param args: arguments to pass to coro 164 | :param kwargs: arguments to pass to coro 165 | :return: dict containing return from coro call for each plugin 166 | """ 167 | p_list = kwargs.pop('filter_plugins', None) 168 | if p_list is None: 169 | p_list = [p.name for p in self.plugins] 170 | tasks = [] 171 | plugins_list = [] 172 | for plugin in self._plugins: 173 | if plugin.name in p_list: 174 | coro_instance = coro(plugin, *args, **kwargs) 175 | if coro_instance: 176 | try: 177 | tasks.append(self._schedule_coro(coro_instance)) 178 | plugins_list.append(plugin) 179 | except AssertionError: 180 | self.logger.error("Method '%r' on plugin '%s' is not a coroutine" % 181 | (coro, plugin.name)) 182 | if tasks: 183 | ret_list = yield from asyncio.gather(*tasks, loop=self._loop) 184 | # Create result map plugin=>ret 185 | ret_dict = {k: v for k, v in zip(plugins_list, ret_list)} 186 | else: 187 | ret_dict = {} 188 | return ret_dict 189 | 190 | @staticmethod 191 | @asyncio.coroutine 192 | def _call_coro(plugin, coro_name, *args, **kwargs): 193 | try: 194 | coro = getattr(plugin.object, coro_name, None)(*args, **kwargs) 195 | return (yield from coro) 196 | except TypeError: 197 | # Plugin doesn't implement coro_name 198 | return None 199 | 200 | @asyncio.coroutine 201 | def map_plugin_coro(self, coro_name, *args, **kwargs): 202 | """ 203 | Call a plugin declared by plugin by its name 204 | :param coro_name: 205 | :param args: 206 | :param kwargs: 207 | :return: 208 | """ 209 | return (yield from self.map(self._call_coro, coro_name, *args, **kwargs)) 210 | -------------------------------------------------------------------------------- /hbmqtt/plugins/persistence.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2015 Nicolas JOUANIN 2 | # 3 | # See the file license.txt for copying permission. 4 | import asyncio 5 | import sqlite3 6 | import pickle 7 | 8 | 9 | class SQLitePlugin: 10 | def __init__(self, context): 11 | self.context = context 12 | self.conn = None 13 | self.cursor = None 14 | self.db_file = None 15 | try: 16 | self.persistence_config = self.context.config['persistence'] 17 | self.init_db() 18 | except KeyError: 19 | self.context.logger.warning("'persistence' section not found in context configuration") 20 | 21 | def init_db(self): 22 | self.db_file = self.persistence_config.get('file', None) 23 | if not self.db_file: 24 | self.context.logger.warning("'file' persistence parameter not found") 25 | else: 26 | try: 27 | self.conn = sqlite3.connect(self.db_file) 28 | self.cursor = self.conn.cursor() 29 | self.context.logger.info("Database file '%s' opened" % self.db_file) 30 | except Exception as e: 31 | self.context.logger.error("Error while initializing database '%s' : %s" % (self.db_file, e)) 32 | if self.cursor: 33 | self.cursor.execute("CREATE TABLE IF NOT EXISTS session(client_id TEXT PRIMARY KEY, data BLOB)") 34 | 35 | @asyncio.coroutine 36 | def save_session(self, session): 37 | if self.cursor: 38 | dump = pickle.dumps(session) 39 | try: 40 | self.cursor.execute( 41 | "INSERT OR REPLACE INTO session (client_id, data) VALUES (?,?)", (session.client_id, dump)) 42 | self.conn.commit() 43 | except Exception as e: 44 | self.context.logger.error("Failed saving session '%s': %s" % (session, e)) 45 | 46 | @asyncio.coroutine 47 | def find_session(self, client_id): 48 | if self.cursor: 49 | row = self.cursor.execute("SELECT data FROM session where client_id=?", (client_id,)).fetchone() 50 | if row: 51 | return pickle.loads(row[0]) 52 | else: 53 | return None 54 | 55 | @asyncio.coroutine 56 | def del_session(self, client_id): 57 | if self.cursor: 58 | self.cursor.execute("DELETE FROM session where client_id=?", (client_id,)) 59 | self.conn.commit() 60 | 61 | @asyncio.coroutine 62 | def on_broker_post_shutdown(self): 63 | if self.conn: 64 | self.conn.close() 65 | self.context.logger.info("Database file '%s' closed" % self.db_file) 66 | -------------------------------------------------------------------------------- /hbmqtt/plugins/sys/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/njouanin/hbmqtt/8b3e915091192b112c93ce7101ec848eadf7e383/hbmqtt/plugins/sys/__init__.py -------------------------------------------------------------------------------- /hbmqtt/plugins/sys/broker.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2015 Nicolas JOUANIN 2 | # 3 | # See the file license.txt for copying permission. 4 | from datetime import datetime 5 | from hbmqtt.mqtt.packet import PUBLISH 6 | from hbmqtt.codecs import int_to_bytes_str 7 | import asyncio 8 | import sys 9 | from collections import deque 10 | 11 | 12 | DOLLAR_SYS_ROOT = '$SYS/broker/' 13 | STAT_BYTES_SENT = 'bytes_sent' 14 | STAT_BYTES_RECEIVED = 'bytes_received' 15 | STAT_MSG_SENT = 'messages_sent' 16 | STAT_MSG_RECEIVED = 'messages_received' 17 | STAT_PUBLISH_SENT = 'publish_sent' 18 | STAT_PUBLISH_RECEIVED = 'publish_received' 19 | STAT_START_TIME = 'start_time' 20 | STAT_CLIENTS_MAXIMUM = 'clients_maximum' 21 | STAT_CLIENTS_CONNECTED = 'clients_connected' 22 | STAT_CLIENTS_DISCONNECTED = 'clients_disconnected' 23 | 24 | 25 | class BrokerSysPlugin: 26 | def __init__(self, context): 27 | self.context = context 28 | # Broker statistics initialization 29 | self._stats = dict() 30 | self._sys_handle = None 31 | 32 | def _clear_stats(self): 33 | """ 34 | Initializes broker statistics data structures 35 | """ 36 | for stat in (STAT_BYTES_RECEIVED, 37 | STAT_BYTES_SENT, 38 | STAT_MSG_RECEIVED, 39 | STAT_MSG_SENT, 40 | STAT_CLIENTS_MAXIMUM, 41 | STAT_CLIENTS_CONNECTED, 42 | STAT_CLIENTS_DISCONNECTED, 43 | STAT_PUBLISH_RECEIVED, 44 | STAT_PUBLISH_SENT): 45 | self._stats[stat] = 0 46 | 47 | @asyncio.coroutine 48 | def _broadcast_sys_topic(self, topic_basename, data): 49 | return (yield from self.context.broadcast_message(topic_basename, data)) 50 | 51 | def schedule_broadcast_sys_topic(self, topic_basename, data): 52 | return asyncio.ensure_future( 53 | self._broadcast_sys_topic(DOLLAR_SYS_ROOT + topic_basename, data), 54 | loop=self.context.loop 55 | ) 56 | 57 | @asyncio.coroutine 58 | def on_broker_pre_start(self, *args, **kwargs): 59 | self._clear_stats() 60 | 61 | @asyncio.coroutine 62 | def on_broker_post_start(self, *args, **kwargs): 63 | self._stats[STAT_START_TIME] = datetime.now() 64 | from hbmqtt.version import get_version 65 | version = 'HBMQTT version ' + get_version() 66 | self.context.retain_message(DOLLAR_SYS_ROOT + 'version', version.encode()) 67 | 68 | # Start $SYS topics management 69 | try: 70 | sys_interval = int(self.context.config.get('sys_interval', 0)) 71 | if sys_interval > 0: 72 | self.context.logger.debug("Setup $SYS broadcasting every %d secondes" % sys_interval) 73 | self.sys_handle = self.context.loop.call_later(sys_interval, self.broadcast_dollar_sys_topics) 74 | else: 75 | self.context.logger.debug("$SYS disabled") 76 | except KeyError: 77 | pass 78 | # 'sys_internal' config parameter not found 79 | 80 | @asyncio.coroutine 81 | def on_broker_pre_stop(self, *args, **kwargs): 82 | # Stop $SYS topics broadcasting 83 | if self.sys_handle: 84 | self.sys_handle.cancel() 85 | 86 | def broadcast_dollar_sys_topics(self): 87 | """ 88 | Broadcast dynamic $SYS topics updates and reschedule next execution depending on 'sys_interval' config 89 | parameter. 90 | """ 91 | 92 | # Update stats 93 | uptime = datetime.now() - self._stats[STAT_START_TIME] 94 | client_connected = self._stats[STAT_CLIENTS_CONNECTED] 95 | client_disconnected = self._stats[STAT_CLIENTS_DISCONNECTED] 96 | inflight_in = 0 97 | inflight_out = 0 98 | messages_stored = 0 99 | for session in self.context.sessions: 100 | inflight_in += session.inflight_in_count 101 | inflight_out += session.inflight_out_count 102 | messages_stored += session.retained_messages_count 103 | messages_stored += len(self.context.retained_messages) 104 | subscriptions_count = 0 105 | for topic in self.context.subscriptions: 106 | subscriptions_count += len(self.context.subscriptions[topic]) 107 | 108 | # Broadcast updates 109 | tasks = deque() 110 | tasks.append(self.schedule_broadcast_sys_topic('load/bytes/received', int_to_bytes_str(self._stats[STAT_BYTES_RECEIVED]))) 111 | tasks.append(self.schedule_broadcast_sys_topic('load/bytes/sent', int_to_bytes_str(self._stats[STAT_BYTES_SENT]))) 112 | tasks.append(self.schedule_broadcast_sys_topic('messages/received', int_to_bytes_str(self._stats[STAT_MSG_RECEIVED]))) 113 | tasks.append(self.schedule_broadcast_sys_topic('messages/sent', int_to_bytes_str(self._stats[STAT_MSG_SENT]))) 114 | tasks.append(self.schedule_broadcast_sys_topic('time', str(datetime.now()).encode('utf-8'))) 115 | tasks.append(self.schedule_broadcast_sys_topic('uptime', int_to_bytes_str(int(uptime.total_seconds())))) 116 | tasks.append(self.schedule_broadcast_sys_topic('uptime/formated', str(uptime).encode('utf-8'))) 117 | tasks.append(self.schedule_broadcast_sys_topic('clients/connected', int_to_bytes_str(client_connected))) 118 | tasks.append(self.schedule_broadcast_sys_topic('clients/disconnected', int_to_bytes_str(client_disconnected))) 119 | tasks.append(self.schedule_broadcast_sys_topic('clients/maximum', int_to_bytes_str(self._stats[STAT_CLIENTS_MAXIMUM]))) 120 | tasks.append(self.schedule_broadcast_sys_topic('clients/total', int_to_bytes_str(client_connected + client_disconnected))) 121 | tasks.append(self.schedule_broadcast_sys_topic('messages/inflight', int_to_bytes_str(inflight_in + inflight_out))) 122 | tasks.append(self.schedule_broadcast_sys_topic('messages/inflight/in', int_to_bytes_str(inflight_in))) 123 | tasks.append(self.schedule_broadcast_sys_topic('messages/inflight/out', int_to_bytes_str(inflight_out))) 124 | tasks.append(self.schedule_broadcast_sys_topic('messages/inflight/stored', int_to_bytes_str(messages_stored))) 125 | tasks.append(self.schedule_broadcast_sys_topic('messages/publish/received', int_to_bytes_str(self._stats[STAT_PUBLISH_RECEIVED]))) 126 | tasks.append(self.schedule_broadcast_sys_topic('messages/publish/sent', int_to_bytes_str(self._stats[STAT_PUBLISH_SENT]))) 127 | tasks.append(self.schedule_broadcast_sys_topic('messages/retained/count', int_to_bytes_str(len(self.context.retained_messages)))) 128 | tasks.append(self.schedule_broadcast_sys_topic('messages/subscriptions/count', int_to_bytes_str(subscriptions_count))) 129 | 130 | # Wait until broadcasting tasks end 131 | while tasks and tasks[0].done(): 132 | tasks.popleft() 133 | # Reschedule 134 | sys_interval = int(self.context.config['sys_interval']) 135 | self.context.logger.debug("Broadcasting $SYS topics") 136 | self.sys_handle = self.context.loop.call_later(sys_interval, self.broadcast_dollar_sys_topics) 137 | 138 | @asyncio.coroutine 139 | def on_mqtt_packet_received(self, *args, **kwargs): 140 | packet = kwargs.get('packet') 141 | if packet: 142 | packet_size = packet.bytes_length 143 | self._stats[STAT_BYTES_RECEIVED] += packet_size 144 | self._stats[STAT_MSG_RECEIVED] += 1 145 | if packet.fixed_header.packet_type == PUBLISH: 146 | self._stats[STAT_PUBLISH_RECEIVED] += 1 147 | 148 | @asyncio.coroutine 149 | def on_mqtt_packet_sent(self, *args, **kwargs): 150 | packet = kwargs.get('packet') 151 | if packet: 152 | packet_size = packet.bytes_length 153 | self._stats[STAT_BYTES_SENT] += packet_size 154 | self._stats[STAT_MSG_SENT] += 1 155 | if packet.fixed_header.packet_type == PUBLISH: 156 | self._stats[STAT_PUBLISH_SENT] += 1 157 | 158 | @asyncio.coroutine 159 | def on_broker_client_connected(self, *args, **kwargs): 160 | self._stats[STAT_CLIENTS_CONNECTED] += 1 161 | self._stats[STAT_CLIENTS_MAXIMUM] = max(self._stats[STAT_CLIENTS_MAXIMUM], self._stats[STAT_CLIENTS_CONNECTED]) 162 | 163 | @asyncio.coroutine 164 | def on_broker_client_disconnected(self, *args, **kwargs): 165 | self._stats[STAT_CLIENTS_CONNECTED] -= 1 166 | self._stats[STAT_CLIENTS_DISCONNECTED] += 1 167 | -------------------------------------------------------------------------------- /hbmqtt/plugins/topic_checking.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | 4 | class BaseTopicPlugin: 5 | def __init__(self, context): 6 | self.context = context 7 | try: 8 | self.topic_config = self.context.config['topic-check'] 9 | except KeyError: 10 | self.context.logger.warning("'topic-check' section not found in context configuration") 11 | 12 | def topic_filtering(self, *args, **kwargs): 13 | if not self.topic_config: 14 | # auth config section not found 15 | self.context.logger.warning("'auth' section not found in context configuration") 16 | return False 17 | return True 18 | 19 | 20 | class TopicTabooPlugin(BaseTopicPlugin): 21 | def __init__(self, context): 22 | super().__init__(context) 23 | self._taboo = ['prohibited', 'top-secret', 'data/classified'] 24 | 25 | @asyncio.coroutine 26 | def topic_filtering(self, *args, **kwargs): 27 | filter_result = super().topic_filtering(*args, **kwargs) 28 | if filter_result: 29 | session = kwargs.get('session', None) 30 | topic = kwargs.get('topic', None) 31 | if topic: 32 | if session.username != 'admin' and topic in self._taboo: 33 | return False 34 | return True 35 | else: 36 | return False 37 | return filter_result 38 | 39 | 40 | class TopicAccessControlListPlugin(BaseTopicPlugin): 41 | def __init__(self, context): 42 | super().__init__(context) 43 | 44 | @staticmethod 45 | def topic_ac(topic_requested, topic_allowed): 46 | req_split = topic_requested.split('/') 47 | allowed_split = topic_allowed.split('/') 48 | ret = True 49 | for i in range(max(len(req_split), len(allowed_split))): 50 | try: 51 | a_aux = req_split[i] 52 | b_aux = allowed_split[i] 53 | except IndexError: 54 | ret = False 55 | break 56 | if b_aux == '#': 57 | break 58 | elif (b_aux == '+') or (b_aux == a_aux): 59 | continue 60 | else: 61 | ret = False 62 | break 63 | return ret 64 | 65 | @asyncio.coroutine 66 | def topic_filtering(self, *args, **kwargs): 67 | filter_result = super().topic_filtering(*args, **kwargs) 68 | if filter_result: 69 | session = kwargs.get('session', None) 70 | req_topic = kwargs.get('topic', None) 71 | if req_topic: 72 | username = session.username 73 | if username is None: 74 | username = 'anonymous' 75 | allowed_topics = self.topic_config['acl'].get(username, None) 76 | if allowed_topics: 77 | for allowed_topic in allowed_topics: 78 | if self.topic_ac(req_topic, allowed_topic): 79 | return True 80 | return False 81 | else: 82 | return False 83 | else: 84 | return False 85 | -------------------------------------------------------------------------------- /hbmqtt/session.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2015 Nicolas JOUANIN 2 | # 3 | # See the file license.txt for copying permission. 4 | import asyncio 5 | from transitions import Machine 6 | from asyncio import Queue 7 | from collections import OrderedDict 8 | from hbmqtt.mqtt.publish import PublishPacket 9 | from hbmqtt.errors import HBMQTTException 10 | 11 | OUTGOING = 0 12 | INCOMING = 1 13 | 14 | 15 | class ApplicationMessage: 16 | 17 | """ 18 | 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. 19 | """ 20 | 21 | __slots__ = ( 22 | 'packet_id', 'topic', 'qos', 'data', 'retain', 'publish_packet', 23 | 'puback_packet', 'pubrec_packet', 'pubrel_packet', 'pubcomp_packet', 24 | ) 25 | 26 | def __init__(self, packet_id, topic, qos, data, retain): 27 | self.packet_id = packet_id 28 | """ Publish message `packet identifier `_""" 29 | 30 | self.topic = topic 31 | """ Publish message topic""" 32 | 33 | self.qos = qos 34 | """ Publish message Quality of Service""" 35 | 36 | self.data = data 37 | """ Publish message payload data""" 38 | 39 | self.retain = retain 40 | """ Publish message retain flag""" 41 | 42 | self.publish_packet = None 43 | """ :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.""" 44 | 45 | self.puback_packet = None 46 | """ :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.""" 47 | 48 | self.pubrec_packet = None 49 | """ :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.""" 50 | 51 | self.pubrel_packet = None 52 | """ :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.""" 53 | 54 | self.pubcomp_packet = None 55 | """ :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.""" 56 | 57 | def build_publish_packet(self, dup=False): 58 | """ 59 | Build :class:`hbmqtt.mqtt.publish.PublishPacket` from attributes 60 | 61 | :param dup: force dup flag 62 | :return: :class:`hbmqtt.mqtt.publish.PublishPacket` built from ApplicationMessage instance attributes 63 | """ 64 | return PublishPacket.build(self.topic, self.data, self.packet_id, dup, self.qos, self.retain) 65 | 66 | def __eq__(self, other): 67 | return self.packet_id == other.packet_id 68 | 69 | 70 | class IncomingApplicationMessage(ApplicationMessage): 71 | 72 | """ 73 | Incoming :class:`~hbmqtt.session.ApplicationMessage`. 74 | """ 75 | 76 | __slots__ = ('direction',) 77 | 78 | def __init__(self, packet_id, topic, qos, data, retain): 79 | super().__init__(packet_id, topic, qos, data, retain) 80 | self.direction = INCOMING 81 | 82 | 83 | class OutgoingApplicationMessage(ApplicationMessage): 84 | 85 | """ 86 | Outgoing :class:`~hbmqtt.session.ApplicationMessage`. 87 | """ 88 | 89 | __slots__ = ('direction',) 90 | 91 | def __init__(self, packet_id, topic, qos, data, retain): 92 | super().__init__(packet_id, topic, qos, data, retain) 93 | self.direction = OUTGOING 94 | 95 | 96 | class Session: 97 | states = ['new', 'connected', 'disconnected'] 98 | 99 | def __init__(self, loop=None): 100 | self._init_states() 101 | self.remote_address = None 102 | self.remote_port = None 103 | self.client_id = None 104 | self.clean_session = None 105 | self.will_flag = False 106 | self.will_message = None 107 | self.will_qos = None 108 | self.will_retain = None 109 | self.will_topic = None 110 | self.keep_alive = 0 111 | self.publish_retry_delay = 0 112 | self.broker_uri = None 113 | self.username = None 114 | self.password = None 115 | self.cafile = None 116 | self.capath = None 117 | self.cadata = None 118 | self._packet_id = 0 119 | self.parent = 0 120 | if loop is not None: 121 | self._loop = loop 122 | else: 123 | self._loop = asyncio.get_event_loop() 124 | 125 | # Used to store outgoing ApplicationMessage while publish protocol flows 126 | self.inflight_out = OrderedDict() 127 | 128 | # Used to store incoming ApplicationMessage while publish protocol flows 129 | self.inflight_in = OrderedDict() 130 | 131 | # Stores messages retained for this session 132 | self.retained_messages = Queue(loop=self._loop) 133 | 134 | # Stores PUBLISH messages ID received in order and ready for application process 135 | self.delivered_message_queue = Queue(loop=self._loop) 136 | 137 | def _init_states(self): 138 | self.transitions = Machine(states=Session.states, initial='new') 139 | self.transitions.add_transition(trigger='connect', source='new', dest='connected') 140 | self.transitions.add_transition(trigger='connect', source='disconnected', dest='connected') 141 | self.transitions.add_transition(trigger='disconnect', source='connected', dest='disconnected') 142 | self.transitions.add_transition(trigger='disconnect', source='new', dest='disconnected') 143 | self.transitions.add_transition(trigger='disconnect', source='disconnected', dest='disconnected') 144 | 145 | @property 146 | def next_packet_id(self): 147 | self._packet_id += 1 148 | if self._packet_id > 65535: 149 | self._packet_id = 1 150 | while self._packet_id in self.inflight_in or self._packet_id in self.inflight_out: 151 | self._packet_id += 1 152 | if self._packet_id > 65535: 153 | raise HBMQTTException("More than 65525 messages pending. No free packet ID") 154 | 155 | return self._packet_id 156 | 157 | @property 158 | def inflight_in_count(self): 159 | return len(self.inflight_in) 160 | 161 | @property 162 | def inflight_out_count(self): 163 | return len(self.inflight_out) 164 | 165 | @property 166 | def retained_messages_count(self): 167 | return self.retained_messages.qsize() 168 | 169 | def __repr__(self): 170 | return type(self).__name__ + '(clientId={0}, state={1})'.format(self.client_id, self.transitions.state) 171 | 172 | def __getstate__(self): 173 | state = self.__dict__.copy() 174 | # Remove the unpicklable entries. 175 | # del state['transitions'] 176 | del state['retained_messages'] 177 | del state['delivered_message_queue'] 178 | return state 179 | 180 | def __setstate(self, state): 181 | self.__dict__.update(state) 182 | self.retained_messages = Queue() 183 | self.delivered_message_queue = Queue() 184 | 185 | def __eq__(self, other): 186 | return self.client_id == other.client_id 187 | -------------------------------------------------------------------------------- /hbmqtt/utils.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2015 Nicolas JOUANIN 2 | # 3 | # See the file license.txt for copying permission. 4 | import logging 5 | 6 | import yaml 7 | 8 | 9 | logger = logging.getLogger(__name__) 10 | 11 | 12 | def not_in_dict_or_none(dict, key): 13 | """ 14 | Check if a key exists in a map and if it's not None 15 | :param dict: map to look for key 16 | :param key: key to find 17 | :return: true if key is in dict and not None 18 | """ 19 | if key not in dict or dict[key] is None: 20 | return True 21 | else: 22 | return False 23 | 24 | 25 | def format_client_message(session=None, address=None, port=None): 26 | if session: 27 | return "(client id=%s)" % session.client_id 28 | elif address is not None and port is not None: 29 | return "(client @=%s:%d)" % (address, port) 30 | else: 31 | return "(unknown client)" 32 | 33 | 34 | def gen_client_id(): 35 | """ 36 | Generates random client ID 37 | :return: 38 | """ 39 | import random 40 | gen_id = 'hbmqtt/' 41 | 42 | for i in range(7, 23): 43 | gen_id += chr(random.randint(0, 74) + 48) 44 | return gen_id 45 | 46 | 47 | def read_yaml_config(config_file): 48 | config = None 49 | try: 50 | with open(config_file, 'r') as stream: 51 | config = yaml.full_load(stream) if hasattr(yaml, 'full_load') else yaml.load(stream) 52 | except yaml.YAMLError as exc: 53 | logger.error("Invalid config_file %s: %s" % (config_file, exc)) 54 | return config 55 | -------------------------------------------------------------------------------- /hbmqtt/version.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2015 Nicolas JOUANIN 2 | # 3 | # See the file license.txt for copying permission. 4 | import datetime 5 | import os 6 | import subprocess 7 | 8 | 9 | # Version Management from https://gist.github.com/gilsondev/2790884 10 | def get_version(version=None): 11 | "Returns a PEP 386-compliant version number from VERSION." 12 | if version is None: 13 | from hbmqtt import VERSION as version 14 | else: 15 | assert len(version) == 5 16 | assert version[3] in ('alpha', 'beta', 'rc', 'final') 17 | 18 | # Now build the two parts of the version number: 19 | # main = X.Y[.Z] 20 | # sub = .devN - for pre-alpha releases 21 | # | {a|b|c}N - for alpha, beta and rc releases 22 | 23 | parts = 2 if version[2] == 0 else 3 24 | main = '.'.join(str(x) for x in version[:parts]) 25 | 26 | sub = '' 27 | if version[3] == 'alpha' and version[4] == 0: 28 | git_changeset = get_git_changeset() 29 | if git_changeset: 30 | sub = '.dev%s' % git_changeset 31 | 32 | elif version[3] != 'final': 33 | mapping = {'alpha': 'a', 'beta': 'b', 'rc': 'c'} 34 | sub = mapping[version[3]] + str(version[4]) 35 | 36 | return str(main + sub) 37 | 38 | 39 | def get_git_changeset(): 40 | """Returns a numeric identifier of the latest git changeset. 41 | The result is the UTC timestamp of the changeset in YYYYMMDDHHMMSS format. 42 | This value isn't guaranteed to be unique, but collisions are very unlikely, 43 | so it's sufficient for generating the development version numbers. 44 | """ 45 | repo_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 46 | git_log = subprocess.Popen( 47 | 'git log --pretty=format:%ct --quiet -1 HEAD', 48 | stdout=subprocess.PIPE, 49 | stderr=subprocess.PIPE, shell=True, cwd=repo_dir, universal_newlines=True) 50 | timestamp = git_log.communicate()[0] 51 | try: 52 | timestamp = datetime.datetime.utcfromtimestamp(int(timestamp)) 53 | except ValueError: 54 | return None 55 | return timestamp.strftime('%Y%m%d%H%M%S') 56 | -------------------------------------------------------------------------------- /license.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Nicolas JOUANIN 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /readme.rst: -------------------------------------------------------------------------------- 1 | HBMQTT 2 | ====== 3 | 4 | ``HBMQTT`` is an open source `MQTT`_ client and broker implementation. 5 | 6 | Built on top of `asyncio`_, Python's standard asynchronous I/O framework, HBMQTT provides a straightforward API 7 | based on coroutines, making it easy to write highly concurrent applications. 8 | 9 | .. _asyncio: https://docs.python.org/3/library/asyncio.html 10 | 11 | No Longer Maintained 12 | -------------------- 13 | 14 | **HBMQTT is no longer maintained**. There is a fork `aqmtt`_ under active development by different people. `See here for some background`_. 15 | 16 | .. _aqmtt: https://github.com/Yakifo/amqtt 17 | .. _See here for some background: https://github.com/beerfactory/hbmqtt/issues/235 18 | 19 | Features 20 | -------- 21 | 22 | HBMQTT implements the full set of `MQTT 3.1.1`_ protocol specifications and provides the following features: 23 | 24 | - Support QoS 0, QoS 1 and QoS 2 messages flow 25 | - Client auto-reconnection on network lost 26 | - Authentication through password file (more methods can be added through a plugin system) 27 | - Basic ``$SYS`` topics 28 | - TCP and websocket support 29 | - SSL support over TCP and websocket 30 | - Plugin system 31 | 32 | Build status 33 | ------------ 34 | 35 | .. image:: https://travis-ci.org/beerfactory/hbmqtt.svg?branch=master 36 | :target: https://travis-ci.org/beerfactory/hbmqtt 37 | 38 | .. image:: https://coveralls.io/repos/beerfactory/hbmqtt/badge.svg?branch=master&service=github 39 | :target: https://coveralls.io/github/beerfactory/hbmqtt?branch=master 40 | 41 | Project status 42 | -------------- 43 | 44 | .. image:: https://readthedocs.org/projects/hbmqtt/badge/?version=latest 45 | :target: http://hbmqtt.readthedocs.org/en/latest/?badge=latest 46 | :alt: Documentation Status 47 | 48 | .. image:: https://badge.fury.io/py/hbmqtt.svg 49 | :target: https://badge.fury.io/py/hbmqtt 50 | 51 | Getting started 52 | --------------- 53 | 54 | hbmqtt is available on `Pypi `_ and can installed simply using ``pip`` : 55 | :: 56 | 57 | $ pip install hbmqtt 58 | 59 | Documentation is available on `Read the Docs`_. 60 | 61 | Bug reports, patches and suggestions welcome! Just `open an issue`_ or join the `gitter channel`_. 62 | 63 | .. image:: https://badges.gitter.im/Join%20Chat.svg 64 | :target: https://gitter.im/beerfactory/hbmqtt?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge 65 | :alt: 'Join the chat at https://gitter.im/beerfactory/hbmqtt' 66 | 67 | .. _MQTT: http://www.mqtt.org 68 | .. _MQTT 3.1.1: http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html 69 | .. _Read the Docs: http://hbmqtt.readthedocs.org/ 70 | .. _open an issue: https://github.com/beerfactory/hbmqtt/issues/new 71 | .. _gitter channel: https://gitter.im/beerfactory/hbmqtt 72 | -------------------------------------------------------------------------------- /samples/broker_acl.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import asyncio 3 | import os 4 | from hbmqtt.broker import Broker 5 | 6 | logger = logging.getLogger(__name__) 7 | 8 | config = { 9 | 'listeners': { 10 | 'default': { 11 | 'type': 'tcp', 12 | 'bind': '0.0.0.0:1883', 13 | }, 14 | 'ws-mqtt': { 15 | 'bind': '127.0.0.1:8080', 16 | 'type': 'ws', 17 | 'max_connections': 10, 18 | }, 19 | }, 20 | 'sys_interval': 10, 21 | 'auth': { 22 | 'allow-anonymous': True, 23 | 'password-file': os.path.join(os.path.dirname(os.path.realpath(__file__)), "passwd"), 24 | 'plugins': [ 25 | 'auth_file', 'auth_anonymous' 26 | ] 27 | 28 | }, 29 | 'topic-check': { 30 | 'enabled': True, 31 | 'plugins': [ 32 | 'topic_acl' 33 | ], 34 | 'acl': { 35 | # username: [list of allowed topics] 36 | 'test': ['repositories/+/master', 'calendar/#', 'data/memes'], 37 | 'anonymous': [] 38 | } 39 | } 40 | } 41 | 42 | broker = Broker(config) 43 | 44 | 45 | @asyncio.coroutine 46 | def test_coro(): 47 | yield from broker.start() 48 | 49 | 50 | if __name__ == '__main__': 51 | formatter = "[%(asctime)s] :: %(levelname)s :: %(name)s :: %(message)s" 52 | logging.basicConfig(level=logging.INFO, format=formatter) 53 | asyncio.get_event_loop().run_until_complete(test_coro()) 54 | asyncio.get_event_loop().run_forever() 55 | -------------------------------------------------------------------------------- /samples/broker_start.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import asyncio 3 | import os 4 | from hbmqtt.broker import Broker 5 | 6 | logger = logging.getLogger(__name__) 7 | 8 | config = { 9 | 'listeners': { 10 | 'default': { 11 | 'type': 'tcp', 12 | 'bind': '0.0.0.0:1883', 13 | }, 14 | 'ws-mqtt': { 15 | 'bind': '127.0.0.1:8080', 16 | 'type': 'ws', 17 | 'max_connections': 10, 18 | }, 19 | }, 20 | 'sys_interval': 10, 21 | 'auth': { 22 | 'allow-anonymous': True, 23 | 'password-file': os.path.join(os.path.dirname(os.path.realpath(__file__)), "passwd"), 24 | 'plugins': [ 25 | 'auth_file', 'auth_anonymous' 26 | ] 27 | }, 28 | 'topic-check': { 29 | 'enabled': False 30 | } 31 | } 32 | 33 | broker = Broker(config) 34 | 35 | 36 | @asyncio.coroutine 37 | def test_coro(): 38 | yield from broker.start() 39 | #yield from asyncio.sleep(5) 40 | #yield from broker.shutdown() 41 | 42 | 43 | if __name__ == '__main__': 44 | formatter = "[%(asctime)s] :: %(levelname)s :: %(name)s :: %(message)s" 45 | #formatter = "%(asctime)s :: %(levelname)s :: %(message)s" 46 | logging.basicConfig(level=logging.INFO, format=formatter) 47 | asyncio.get_event_loop().run_until_complete(test_coro()) 48 | asyncio.get_event_loop().run_forever() 49 | -------------------------------------------------------------------------------- /samples/broker_taboo.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import asyncio 3 | import os 4 | from hbmqtt.broker import Broker 5 | 6 | logger = logging.getLogger(__name__) 7 | 8 | config = { 9 | 'listeners': { 10 | 'default': { 11 | 'type': 'tcp', 12 | 'bind': '0.0.0.0:1883', 13 | }, 14 | 'ws-mqtt': { 15 | 'bind': '127.0.0.1:8080', 16 | 'type': 'ws', 17 | 'max_connections': 10, 18 | }, 19 | }, 20 | 'sys_interval': 10, 21 | 'auth': { 22 | 'allow-anonymous': True, 23 | 'password-file': os.path.join(os.path.dirname(os.path.realpath(__file__)), "passwd"), 24 | 'plugins': [ 25 | 'auth_file', 'auth_anonymous' 26 | ] 27 | 28 | }, 29 | 'topic-check': { 30 | 'enabled': True, 31 | 'plugins': [ 32 | 'topic_taboo' 33 | ] 34 | } 35 | } 36 | 37 | broker = Broker(config) 38 | 39 | 40 | @asyncio.coroutine 41 | def test_coro(): 42 | yield from broker.start() 43 | 44 | 45 | if __name__ == '__main__': 46 | formatter = "[%(asctime)s] :: %(levelname)s :: %(name)s :: %(message)s" 47 | logging.basicConfig(level=logging.INFO, format=formatter) 48 | asyncio.get_event_loop().run_until_complete(test_coro()) 49 | asyncio.get_event_loop().run_forever() 50 | -------------------------------------------------------------------------------- /samples/client_keepalive.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import asyncio 3 | 4 | from hbmqtt.client import MQTTClient 5 | 6 | 7 | # 8 | # This sample shows a client running idle. 9 | # Meanwhile, keepalive is managed through PING messages sent every 5 seconds 10 | # 11 | 12 | 13 | logger = logging.getLogger(__name__) 14 | 15 | config = { 16 | 'keep_alive': 5, 17 | 'ping_delay': 1, 18 | } 19 | C = MQTTClient(config=config) 20 | 21 | 22 | @asyncio.coroutine 23 | def test_coro(): 24 | yield from C.connect('mqtt://test.mosquitto.org:1883/') 25 | yield from asyncio.sleep(18) 26 | 27 | yield from C.disconnect() 28 | 29 | 30 | if __name__ == '__main__': 31 | formatter = "[%(asctime)s] {%(filename)s:%(lineno)d} %(levelname)s - %(message)s" 32 | logging.basicConfig(level=logging.DEBUG, format=formatter) 33 | asyncio.get_event_loop().run_until_complete(test_coro()) 34 | -------------------------------------------------------------------------------- /samples/client_publish.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import asyncio 3 | 4 | from hbmqtt.client import MQTTClient, ConnectException 5 | from hbmqtt.mqtt.constants import QOS_1, QOS_2 6 | 7 | 8 | # 9 | # This sample shows how to publish messages to broker using different QOS 10 | # Debug outputs shows the message flows 11 | # 12 | 13 | logger = logging.getLogger(__name__) 14 | 15 | config = { 16 | 'will': { 17 | 'topic': '/will/client', 18 | 'message': b'Dead or alive', 19 | 'qos': 0x01, 20 | 'retain': True 21 | } 22 | } 23 | 24 | 25 | @asyncio.coroutine 26 | def test_coro(): 27 | C = MQTTClient() 28 | yield from C.connect('mqtt://test.mosquitto.org/') 29 | tasks = [ 30 | asyncio.ensure_future(C.publish('a/b', b'TEST MESSAGE WITH QOS_0')), 31 | asyncio.ensure_future(C.publish('a/b', b'TEST MESSAGE WITH QOS_1', qos=QOS_1)), 32 | asyncio.ensure_future(C.publish('a/b', b'TEST MESSAGE WITH QOS_2', qos=QOS_2)), 33 | ] 34 | yield from asyncio.wait(tasks) 35 | logger.info("messages published") 36 | yield from C.disconnect() 37 | 38 | 39 | @asyncio.coroutine 40 | def test_coro2(): 41 | try: 42 | C = MQTTClient() 43 | yield from C.connect('mqtt://test.mosquitto.org:1883/') 44 | yield from C.publish('a/b', b'TEST MESSAGE WITH QOS_0', qos=0x00) 45 | yield from C.publish('a/b', b'TEST MESSAGE WITH QOS_1', qos=0x01) 46 | yield from C.publish('a/b', b'TEST MESSAGE WITH QOS_2', qos=0x02) 47 | logger.info("messages published") 48 | yield from C.disconnect() 49 | except ConnectException as ce: 50 | logger.error("Connection failed: %s" % ce) 51 | asyncio.get_event_loop().stop() 52 | 53 | 54 | if __name__ == '__main__': 55 | formatter = "[%(asctime)s] %(name)s {%(filename)s:%(lineno)d} %(levelname)s - %(message)s" 56 | formatter = "%(message)s" 57 | logging.basicConfig(level=logging.DEBUG, format=formatter) 58 | asyncio.get_event_loop().run_until_complete(test_coro()) 59 | asyncio.get_event_loop().run_until_complete(test_coro2()) 60 | -------------------------------------------------------------------------------- /samples/client_publish_acl.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import asyncio 3 | 4 | from hbmqtt.client import MQTTClient, ConnectException 5 | 6 | 7 | # 8 | # This sample shows how to publish messages to broker using different QOS 9 | # Debug outputs shows the message flows 10 | # 11 | 12 | logger = logging.getLogger(__name__) 13 | 14 | 15 | @asyncio.coroutine 16 | def test_coro(): 17 | try: 18 | C = MQTTClient() 19 | yield from C.connect('mqtt://0.0.0.0:1883') 20 | yield from C.publish('data/classified', b'TOP SECRET', qos=0x01) 21 | yield from C.publish('data/memes', b'REAL FUN', qos=0x01) 22 | yield from C.publish('repositories/hbmqtt/master', b'NEW STABLE RELEASE', qos=0x01) 23 | yield from C.publish('repositories/hbmqtt/devel', b'THIS NEEDS TO BE CHECKED', qos=0x01) 24 | yield from C.publish('calendar/hbmqtt/releases', b'NEW RELEASE', qos=0x01) 25 | logger.info("messages published") 26 | yield from C.disconnect() 27 | except ConnectException as ce: 28 | logger.error("Connection failed: %s" % ce) 29 | asyncio.get_event_loop().stop() 30 | 31 | 32 | if __name__ == '__main__': 33 | formatter = "[%(asctime)s] %(name)s {%(filename)s:%(lineno)d} %(levelname)s - %(message)s" 34 | formatter = "%(message)s" 35 | logging.basicConfig(level=logging.DEBUG, format=formatter) 36 | asyncio.get_event_loop().run_until_complete(test_coro()) 37 | -------------------------------------------------------------------------------- /samples/client_publish_ssl.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import asyncio 3 | 4 | from hbmqtt.client import MQTTClient 5 | from hbmqtt.mqtt.constants import QOS_1, QOS_2 6 | 7 | 8 | # 9 | # This sample shows how to publish messages to broker using different QOS 10 | # Debug outputs shows the message flows 11 | # 12 | 13 | logger = logging.getLogger(__name__) 14 | 15 | config = { 16 | 'will': { 17 | 'topic': '/will/client', 18 | 'message': b'Dead or alive', 19 | 'qos': 0x01, 20 | 'retain': True, 21 | }, 22 | } 23 | C = MQTTClient(config=config) 24 | #C = MQTTClient() 25 | 26 | 27 | @asyncio.coroutine 28 | def test_coro(): 29 | yield from C.connect('mqtts://test.mosquitto.org/', cafile='mosquitto.org.crt') 30 | tasks = [ 31 | asyncio.ensure_future(C.publish('a/b', b'TEST MESSAGE WITH QOS_0')), 32 | asyncio.ensure_future(C.publish('a/b', b'TEST MESSAGE WITH QOS_1', qos=QOS_1)), 33 | asyncio.ensure_future(C.publish('a/b', b'TEST MESSAGE WITH QOS_2', qos=QOS_2)), 34 | ] 35 | yield from asyncio.wait(tasks) 36 | logger.info("messages published") 37 | yield from C.disconnect() 38 | 39 | 40 | if __name__ == '__main__': 41 | formatter = "[%(asctime)s] {%(filename)s:%(lineno)d} %(levelname)s - %(message)s" 42 | logging.basicConfig(level=logging.DEBUG, format=formatter) 43 | asyncio.get_event_loop().run_until_complete(test_coro()) 44 | -------------------------------------------------------------------------------- /samples/client_publish_ws.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import asyncio 3 | 4 | from hbmqtt.client import MQTTClient 5 | from hbmqtt.mqtt.constants import QOS_1, QOS_2 6 | 7 | 8 | # 9 | # This sample shows how to publish messages to broker using different QOS 10 | # Debug outputs shows the message flows 11 | # 12 | 13 | logger = logging.getLogger(__name__) 14 | 15 | config = { 16 | 'will': { 17 | 'topic': '/will/client', 18 | 'message': b'Dead or alive', 19 | 'qos': 0x01, 20 | 'retain': True 21 | }, 22 | 'capath': '.', 23 | } 24 | C = MQTTClient(config=config) 25 | #C = MQTTClient() 26 | 27 | 28 | @asyncio.coroutine 29 | def test_coro(): 30 | yield from C.connect('wss://test.mosquitto.org:8081/', cafile='mosquitto.org.crt') 31 | tasks = [ 32 | asyncio.ensure_future(C.publish('a/b', b'TEST MESSAGE WITH QOS_0')), 33 | asyncio.ensure_future(C.publish('a/b', b'TEST MESSAGE WITH QOS_1', qos=QOS_1)), 34 | asyncio.ensure_future(C.publish('a/b', b'TEST MESSAGE WITH QOS_2', qos=QOS_2)), 35 | ] 36 | yield from asyncio.wait(tasks) 37 | logger.info("messages published") 38 | yield from C.disconnect() 39 | 40 | 41 | if __name__ == '__main__': 42 | formatter = "[%(asctime)s] {%(filename)s:%(lineno)d} %(levelname)s - %(message)s" 43 | logging.basicConfig(level=logging.DEBUG, format=formatter) 44 | asyncio.get_event_loop().run_until_complete(test_coro()) 45 | -------------------------------------------------------------------------------- /samples/client_subscribe.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import asyncio 3 | 4 | from hbmqtt.client import MQTTClient, ClientException 5 | from hbmqtt.mqtt.constants import QOS_1, QOS_2 6 | 7 | 8 | # 9 | # This sample shows how to subscbribe a topic and receive data from incoming messages 10 | # It subscribes to '$SYS/broker/uptime' topic and displays the first ten values returned 11 | # by the broker. 12 | # 13 | 14 | logger = logging.getLogger(__name__) 15 | 16 | 17 | @asyncio.coroutine 18 | def uptime_coro(): 19 | C = MQTTClient() 20 | yield from C.connect('mqtt://test.mosquitto.org/') 21 | # Subscribe to '$SYS/broker/uptime' with QOS=1 22 | yield from C.subscribe([ 23 | ('$SYS/broker/uptime', QOS_1), 24 | ('$SYS/broker/load/#', QOS_2), 25 | ]) 26 | logger.info("Subscribed") 27 | try: 28 | for i in range(1, 100): 29 | message = yield from C.deliver_message() 30 | packet = message.publish_packet 31 | print("%d: %s => %s" % (i, packet.variable_header.topic_name, str(packet.payload.data))) 32 | yield from C.unsubscribe(['$SYS/broker/uptime', '$SYS/broker/load/#']) 33 | logger.info("UnSubscribed") 34 | yield from C.disconnect() 35 | except ClientException as ce: 36 | logger.error("Client exception: %s" % ce) 37 | 38 | 39 | if __name__ == '__main__': 40 | formatter = "[%(asctime)s] {%(filename)s:%(lineno)d} %(levelname)s - %(message)s" 41 | logging.basicConfig(level=logging.INFO, format=formatter) 42 | asyncio.get_event_loop().run_until_complete(uptime_coro()) 43 | -------------------------------------------------------------------------------- /samples/client_subscribe_acl.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import asyncio 3 | 4 | from hbmqtt.client import MQTTClient, ClientException 5 | from hbmqtt.mqtt.constants import QOS_1 6 | 7 | 8 | # 9 | # This sample shows how to subscbribe a topic and receive data from incoming messages 10 | # It subscribes to '$SYS/broker/uptime' topic and displays the first ten values returned 11 | # by the broker. 12 | # 13 | 14 | logger = logging.getLogger(__name__) 15 | 16 | 17 | @asyncio.coroutine 18 | def uptime_coro(): 19 | C = MQTTClient() 20 | yield from C.connect('mqtt://test:test@0.0.0.0:1883') 21 | # yield from C.connect('mqtt://0.0.0.0:1883') 22 | # Subscribe to '$SYS/broker/uptime' with QOS=1 23 | yield from C.subscribe([ 24 | ('data/memes', QOS_1), # Topic allowed 25 | ('data/classified', QOS_1), # Topic forbidden 26 | ('repositories/hbmqtt/master', QOS_1), # Topic allowed 27 | ('repositories/hbmqtt/devel', QOS_1), # Topic forbidden 28 | ('calendar/hbmqtt/releases', QOS_1), # Topic allowed 29 | ]) 30 | logger.info("Subscribed") 31 | try: 32 | for i in range(1, 100): 33 | message = yield from C.deliver_message() 34 | packet = message.publish_packet 35 | print("%d: %s => %s" % (i, packet.variable_header.topic_name, str(packet.payload.data))) 36 | yield from C.unsubscribe(['$SYS/broker/uptime', '$SYS/broker/load/#']) 37 | logger.info("UnSubscribed") 38 | yield from C.disconnect() 39 | except ClientException as ce: 40 | logger.error("Client exception: %s" % ce) 41 | 42 | 43 | if __name__ == '__main__': 44 | formatter = "[%(asctime)s] {%(filename)s:%(lineno)d} %(levelname)s - %(message)s" 45 | logging.basicConfig(level=logging.INFO, format=formatter) 46 | asyncio.get_event_loop().run_until_complete(uptime_coro()) 47 | -------------------------------------------------------------------------------- /samples/mosquitto.org.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIC8DCCAlmgAwIBAgIJAOD63PlXjJi8MA0GCSqGSIb3DQEBBQUAMIGQMQswCQYD 3 | VQQGEwJHQjEXMBUGA1UECAwOVW5pdGVkIEtpbmdkb20xDjAMBgNVBAcMBURlcmJ5 4 | MRIwEAYDVQQKDAlNb3NxdWl0dG8xCzAJBgNVBAsMAkNBMRYwFAYDVQQDDA1tb3Nx 5 | dWl0dG8ub3JnMR8wHQYJKoZIhvcNAQkBFhByb2dlckBhdGNob28ub3JnMB4XDTEy 6 | MDYyOTIyMTE1OVoXDTIyMDYyNzIyMTE1OVowgZAxCzAJBgNVBAYTAkdCMRcwFQYD 7 | VQQIDA5Vbml0ZWQgS2luZ2RvbTEOMAwGA1UEBwwFRGVyYnkxEjAQBgNVBAoMCU1v 8 | c3F1aXR0bzELMAkGA1UECwwCQ0ExFjAUBgNVBAMMDW1vc3F1aXR0by5vcmcxHzAd 9 | BgkqhkiG9w0BCQEWEHJvZ2VyQGF0Y2hvby5vcmcwgZ8wDQYJKoZIhvcNAQEBBQAD 10 | gY0AMIGJAoGBAMYkLmX7SqOT/jJCZoQ1NWdCrr/pq47m3xxyXcI+FLEmwbE3R9vM 11 | rE6sRbP2S89pfrCt7iuITXPKycpUcIU0mtcT1OqxGBV2lb6RaOT2gC5pxyGaFJ+h 12 | A+GIbdYKO3JprPxSBoRponZJvDGEZuM3N7p3S/lRoi7G5wG5mvUmaE5RAgMBAAGj 13 | UDBOMB0GA1UdDgQWBBTad2QneVztIPQzRRGj6ZHKqJTv5jAfBgNVHSMEGDAWgBTa 14 | d2QneVztIPQzRRGj6ZHKqJTv5jAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBQUA 15 | A4GBAAqw1rK4NlRUCUBLhEFUQasjP7xfFqlVbE2cRy0Rs4o3KS0JwzQVBwG85xge 16 | REyPOFdGdhBY2P1FNRy0MDr6xr+D2ZOwxs63dG1nnAnWZg7qwoLgpZ4fESPD3PkA 17 | 1ZgKJc2zbSQ9fCPxt2W3mdVav66c6fsb7els2W2Iz7gERJSX 18 | -----END CERTIFICATE----- 19 | -------------------------------------------------------------------------------- /samples/passwd: -------------------------------------------------------------------------------- 1 | # Test user with 'test' password encrypted with sha-512 2 | test:$6$l4zQEHEcowc1Pnv4$HHrh8xnsZoLItQ8BmpFHM4r6q5UqK3DnXp2GaTm5zp5buQ7NheY3Xt9f6godVKbEtA.hOC7IEDwnok3pbAOip. -------------------------------------------------------------------------------- /scripts/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/njouanin/hbmqtt/8b3e915091192b112c93ce7101ec848eadf7e383/scripts/__init__.py -------------------------------------------------------------------------------- /scripts/broker_script.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2015 Nicolas JOUANIN 2 | # 3 | # See the file license.txt for copying permission. 4 | """ 5 | HBMQTT - MQTT 3.1.1 broker 6 | 7 | Usage: 8 | hbmqtt --version 9 | hbmqtt (-h | --help) 10 | hbmqtt [-c ] [-d] 11 | 12 | Options: 13 | -h --help Show this screen. 14 | --version Show version. 15 | -c Broker configuration file (YAML format) 16 | -d Enable debug messages 17 | """ 18 | 19 | import sys 20 | import logging 21 | import asyncio 22 | import os 23 | from hbmqtt.broker import Broker 24 | from hbmqtt.version import get_version 25 | from docopt import docopt 26 | from hbmqtt.utils import read_yaml_config 27 | 28 | 29 | default_config = { 30 | 'listeners': { 31 | 'default': { 32 | 'type': 'tcp', 33 | 'bind': '0.0.0.0:1883', 34 | }, 35 | }, 36 | 'sys_interval': 10, 37 | 'auth': { 38 | 'allow-anonymous': True, 39 | 'password-file': os.path.join(os.path.dirname(os.path.realpath(__file__)), "passwd"), 40 | 'plugins': [ 41 | 'auth_file', 'auth_anonymous' 42 | ] 43 | }, 44 | 'topic-check': { 45 | 'enabled': False 46 | } 47 | } 48 | 49 | logger = logging.getLogger(__name__) 50 | 51 | 52 | def main(*args, **kwargs): 53 | if sys.version_info[:2] < (3, 4): 54 | logger.fatal("Error: Python 3.4+ is required") 55 | sys.exit(-1) 56 | 57 | arguments = docopt(__doc__, version=get_version()) 58 | formatter = "[%(asctime)s] :: %(levelname)s - %(message)s" 59 | 60 | if arguments['-d']: 61 | level = logging.DEBUG 62 | else: 63 | level = logging.INFO 64 | logging.basicConfig(level=level, format=formatter) 65 | 66 | config = None 67 | if arguments['-c']: 68 | config = read_yaml_config(arguments['-c']) 69 | else: 70 | config = read_yaml_config(os.path.join(os.path.dirname(os.path.realpath(__file__)), 'default_broker.yaml')) 71 | logger.debug("Using default configuration") 72 | loop = asyncio.get_event_loop() 73 | broker = Broker(config) 74 | try: 75 | loop.run_until_complete(broker.start()) 76 | loop.run_forever() 77 | except KeyboardInterrupt: 78 | loop.run_until_complete(broker.shutdown()) 79 | finally: 80 | loop.close() 81 | 82 | 83 | if __name__ == "__main__": 84 | main() 85 | -------------------------------------------------------------------------------- /scripts/default_broker.yaml: -------------------------------------------------------------------------------- 1 | listeners: 2 | default: 3 | type: tcp 4 | bind: 0.0.0.0:1883 5 | sys_interval: 20 6 | auth: 7 | allow-anonymous: true 8 | plugins: 9 | - auth_file 10 | - auth_anonymous 11 | topic-check: 12 | enabled: False -------------------------------------------------------------------------------- /scripts/default_client.yaml: -------------------------------------------------------------------------------- 1 | keep_alive: 10 2 | ping_delay: 1 3 | default_qos: 0 4 | default_retain: false 5 | auto_reconnect: false 6 | reconnect_max_interval: 10 7 | reconnect_retries: 2 8 | -------------------------------------------------------------------------------- /scripts/pub_script.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2015 Nicolas JOUANIN 2 | # 3 | # See the file license.txt for copying permission. 4 | """ 5 | hbmqtt_pub - MQTT 3.1.1 publisher 6 | 7 | Usage: 8 | hbmqtt_pub --version 9 | hbmqtt_pub (-h | --help) 10 | 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] ] [--extra-headers HEADER] [-r] 11 | 12 | Options: 13 | -h --help Show this screen. 14 | --version Show version. 15 | --url BROKER_URL Broker connection URL (musr conform to MQTT URI scheme (see https://github.com/mqtt/mqtt.github.io/wiki/URI-Scheme>) 16 | -c CONFIG_FILE Broker configuration file (YAML format) 17 | -i CLIENT_ID Id to use as client ID. 18 | -q | --qos QOS Quality of service to use for the message, from 0, 1 and 2. Defaults to 0. 19 | -r Set retain flag on connect 20 | -t TOPIC Message topic 21 | -m MESSAGE Message data to send 22 | -f FILE Read file by line and publish message for each line 23 | -s Read from stdin and publish message for each line 24 | -k KEEP_ALIVE Keep alive timeout in second 25 | --clean-session Clean session on connect (defaults to False) 26 | --ca-file CAFILE] CA file 27 | --ca-path CAPATH] CA Path 28 | --ca-data CADATA CA data 29 | --will-topic WILL_TOPIC 30 | --will-message WILL_MESSAGE 31 | --will-qos WILL_QOS 32 | --will-retain 33 | --extra-headers EXTRA_HEADERS JSON object with key-value pairs of additional headers for websocket connections 34 | -d Enable debug messages 35 | """ 36 | 37 | import sys 38 | import logging 39 | import asyncio 40 | import os 41 | import json 42 | from hbmqtt.client import MQTTClient, ConnectException 43 | from hbmqtt.version import get_version 44 | from docopt import docopt 45 | from hbmqtt.utils import read_yaml_config 46 | 47 | 48 | logger = logging.getLogger(__name__) 49 | 50 | 51 | def _gen_client_id(): 52 | import os 53 | import socket 54 | pid = os.getpid() 55 | hostname = socket.gethostname() 56 | return "hbmqtt_pub/%d-%s" % (pid, hostname) 57 | 58 | 59 | def _get_qos(arguments): 60 | try: 61 | return int(arguments['--qos'][0]) 62 | except: 63 | return None 64 | 65 | def _get_extra_headers(arguments): 66 | try: 67 | return json.loads(arguments['--extra-headers']) 68 | except: 69 | return {} 70 | 71 | def _get_message(arguments): 72 | if arguments['-n']: 73 | yield b'' 74 | if arguments['-m']: 75 | yield arguments['-m'].encode(encoding='utf-8') 76 | if arguments['-f']: 77 | try: 78 | with open(arguments['-f'], 'r') as f: 79 | for line in f: 80 | yield line.encode(encoding='utf-8') 81 | except: 82 | logger.error("Failed to read file '%s'" % arguments['-f']) 83 | if arguments['-l']: 84 | import sys 85 | for line in sys.stdin: 86 | if line: 87 | yield line.encode(encoding='utf-8') 88 | if arguments['-s']: 89 | import sys 90 | message = bytearray() 91 | for line in sys.stdin: 92 | message.extend(line.encode(encoding='utf-8')) 93 | yield message 94 | 95 | 96 | @asyncio.coroutine 97 | def do_pub(client, arguments): 98 | running_tasks = [] 99 | 100 | try: 101 | logger.info("%s Connecting to broker" % client.client_id) 102 | 103 | yield from client.connect(uri=arguments['--url'], 104 | cleansession=arguments['--clean-session'], 105 | cafile=arguments['--ca-file'], 106 | capath=arguments['--ca-path'], 107 | cadata=arguments['--ca-data'], 108 | extra_headers=_get_extra_headers(arguments)) 109 | qos = _get_qos(arguments) 110 | topic = arguments['-t'] 111 | retain = arguments['-r'] 112 | for message in _get_message(arguments): 113 | logger.info("%s Publishing to '%s'" % (client.client_id, topic)) 114 | task = asyncio.ensure_future(client.publish(topic, message, qos, retain)) 115 | running_tasks.append(task) 116 | if running_tasks: 117 | yield from asyncio.wait(running_tasks) 118 | yield from client.disconnect() 119 | logger.info("%s Disconnected from broker" % client.client_id) 120 | except KeyboardInterrupt: 121 | yield from client.disconnect() 122 | logger.info("%s Disconnected from broker" % client.client_id) 123 | except ConnectException as ce: 124 | logger.fatal("connection to '%s' failed: %r" % (arguments['--url'], ce)) 125 | except asyncio.CancelledError as cae: 126 | logger.fatal("Publish canceled due to prvious error") 127 | 128 | 129 | def main(*args, **kwargs): 130 | if sys.version_info[:2] < (3, 4): 131 | logger.fatal("Error: Python 3.4+ is required") 132 | sys.exit(-1) 133 | 134 | arguments = docopt(__doc__, version=get_version()) 135 | #print(arguments) 136 | formatter = "[%(asctime)s] :: %(levelname)s - %(message)s" 137 | 138 | if arguments['-d']: 139 | level = logging.DEBUG 140 | else: 141 | level = logging.INFO 142 | logging.basicConfig(level=level, format=formatter) 143 | 144 | config = None 145 | if arguments['-c']: 146 | config = read_yaml_config(arguments['-c']) 147 | else: 148 | config = read_yaml_config(os.path.join(os.path.dirname(os.path.realpath(__file__)), 'default_client.yaml')) 149 | logger.debug("Using default configuration") 150 | loop = asyncio.get_event_loop() 151 | 152 | client_id = arguments.get("-i", None) 153 | if not client_id: 154 | client_id = _gen_client_id() 155 | 156 | if arguments['-k']: 157 | config['keep_alive'] = int(arguments['-k']) 158 | 159 | if arguments['--will-topic'] and arguments['--will-message'] and arguments['--will-qos']: 160 | config['will'] = dict() 161 | config['will']['topic'] = arguments['--will-topic'] 162 | config['will']['message'] = arguments['--will-message'].encode('utf-8') 163 | config['will']['qos'] = int(arguments['--will-qos']) 164 | config['will']['retain'] = arguments['--will-retain'] 165 | 166 | client = MQTTClient(client_id=client_id, config=config, loop=loop) 167 | loop.run_until_complete(do_pub(client, arguments)) 168 | loop.close() 169 | 170 | 171 | if __name__ == "__main__": 172 | main() 173 | -------------------------------------------------------------------------------- /scripts/sub_script.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2015 Nicolas JOUANIN 2 | # 3 | # See the file license.txt for copying permission. 4 | """ 5 | hbmqtt_sub - MQTT 3.1.1 publisher 6 | 7 | Usage: 8 | hbmqtt_sub --version 9 | hbmqtt_sub (-h | --help) 10 | 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] ] [--extra-headers HEADER] 11 | 12 | Options: 13 | -h --help Show this screen. 14 | --version Show version. 15 | --url BROKER_URL Broker connection URL (musr conform to MQTT URI scheme (see https://github.com/mqtt/mqtt.github.io/wiki/URI-Scheme>) 16 | -c CONFIG_FILE Broker configuration file (YAML format) 17 | -i CLIENT_ID Id to use as client ID. 18 | -n COUNT Number of messages to read before ending. 19 | -q | --qos QOS Quality of service desired to receive messages, from 0, 1 and 2. Defaults to 0. 20 | -t TOPIC... Topic filter to subcribe 21 | -k KEEP_ALIVE Keep alive timeout in second 22 | --clean-session Clean session on connect (defaults to False) 23 | --ca-file CAFILE] CA file 24 | --ca-path CAPATH] CA Path 25 | --ca-data CADATA CA data 26 | --will-topic WILL_TOPIC 27 | --will-message WILL_MESSAGE 28 | --will-qos WILL_QOS 29 | --will-retain 30 | --extra-headers EXTRA_HEADERS JSON object with key-value pairs of additional headers for websocket connections 31 | -d Enable debug messages 32 | """ 33 | 34 | import sys 35 | import logging 36 | import asyncio 37 | import os 38 | import json 39 | from hbmqtt.client import MQTTClient, ConnectException 40 | from hbmqtt.errors import MQTTException 41 | from hbmqtt.version import get_version 42 | from docopt import docopt 43 | from hbmqtt.mqtt.constants import QOS_0 44 | from hbmqtt.utils import read_yaml_config 45 | 46 | logger = logging.getLogger(__name__) 47 | 48 | 49 | def _gen_client_id(): 50 | import os 51 | import socket 52 | pid = os.getpid() 53 | hostname = socket.gethostname() 54 | return "hbmqtt_sub/%d-%s" % (pid, hostname) 55 | 56 | 57 | def _get_qos(arguments): 58 | try: 59 | return int(arguments['--qos'][0]) 60 | except: 61 | return QOS_0 62 | 63 | def _get_extra_headers(arguments): 64 | try: 65 | return json.loads(arguments['--extra-headers']) 66 | except: 67 | return {} 68 | 69 | 70 | @asyncio.coroutine 71 | def do_sub(client, arguments): 72 | 73 | try: 74 | yield from client.connect(uri=arguments['--url'], 75 | cleansession=arguments['--clean-session'], 76 | cafile=arguments['--ca-file'], 77 | capath=arguments['--ca-path'], 78 | cadata=arguments['--ca-data'], 79 | extra_headers=_get_extra_headers(arguments)) 80 | qos = _get_qos(arguments) 81 | filters = [] 82 | for topic in arguments['-t']: 83 | filters.append((topic, qos)) 84 | yield from client.subscribe(filters) 85 | if arguments['-n']: 86 | max_count = int(arguments['-n']) 87 | else: 88 | max_count = None 89 | count = 0 90 | while True: 91 | if max_count and count >= max_count: 92 | break 93 | try: 94 | message = yield from client.deliver_message() 95 | count += 1 96 | sys.stdout.buffer.write(message.publish_packet.data) 97 | sys.stdout.write('\n') 98 | except MQTTException: 99 | logger.debug("Error reading packet") 100 | yield from client.disconnect() 101 | except KeyboardInterrupt: 102 | yield from client.disconnect() 103 | except ConnectException as ce: 104 | logger.fatal("connection to '%s' failed: %r" % (arguments['--url'], ce)) 105 | except asyncio.CancelledError as cae: 106 | logger.fatal("Publish canceled due to prvious error") 107 | 108 | 109 | def main(*args, **kwargs): 110 | if sys.version_info[:2] < (3, 4): 111 | logger.fatal("Error: Python 3.4+ is required") 112 | sys.exit(-1) 113 | 114 | arguments = docopt(__doc__, version=get_version()) 115 | #print(arguments) 116 | formatter = "[%(asctime)s] :: %(levelname)s - %(message)s" 117 | 118 | if arguments['-d']: 119 | level = logging.DEBUG 120 | else: 121 | level = logging.INFO 122 | logging.basicConfig(level=level, format=formatter) 123 | 124 | config = None 125 | if arguments['-c']: 126 | config = read_yaml_config(arguments['-c']) 127 | else: 128 | config = read_yaml_config(os.path.join(os.path.dirname(os.path.realpath(__file__)), 'default_client.yaml')) 129 | logger.debug(os.path.join(os.path.dirname(os.path.realpath(__file__)), 'default_client.yaml')) 130 | logger.debug("Using default configuration") 131 | loop = asyncio.get_event_loop() 132 | 133 | client_id = arguments.get("-i", None) 134 | if not client_id: 135 | client_id = _gen_client_id() 136 | 137 | if arguments['-k']: 138 | config['keep_alive'] = int(arguments['-k']) 139 | 140 | if arguments['--will-topic'] and arguments['--will-message'] and arguments['--will-qos']: 141 | config['will'] = dict() 142 | config['will']['topic'] = arguments['--will-topic'] 143 | config['will']['message'] = arguments['--will-message'].encode('utf-8') 144 | config['will']['qos'] = int(arguments['--will-qos']) 145 | config['will']['retain'] = arguments['--will-retain'] 146 | 147 | client = MQTTClient(client_id=client_id, config=config, loop=loop) 148 | loop.run_until_complete(do_sub(client, arguments)) 149 | loop.close() 150 | 151 | 152 | if __name__ == "__main__": 153 | main() 154 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | python-tag = py34.py35 -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2015 Nicolas JOUANIN 2 | # 3 | # See the file license.txt for copying permission. 4 | 5 | from setuptools import setup, find_packages 6 | from hbmqtt.version import get_version 7 | 8 | setup( 9 | name="hbmqtt", 10 | version=get_version(), 11 | description="MQTT client/broker using Python 3.4 asyncio library", 12 | author="Nicolas Jouanin", 13 | author_email='nico@beerfactory.org', 14 | url="https://github.com/beerfactory/hbmqtt", 15 | license='MIT', 16 | packages=find_packages(exclude=['tests']), 17 | include_package_data=True, 18 | platforms='all', 19 | install_requires=[ 20 | 'transitions', 21 | 'websockets', 22 | 'passlib', 23 | 'docopt', 24 | 'pyyaml' 25 | ], 26 | classifiers=[ 27 | 'Development Status :: 3 - Alpha', 28 | 'Intended Audience :: Developers', 29 | 'License :: OSI Approved :: MIT License', 30 | 'Operating System :: POSIX', 31 | 'Operating System :: MacOS', 32 | 'Operating System :: Microsoft :: Windows', 33 | 'Programming Language :: Python :: 3.4', 34 | 'Programming Language :: Python :: 3.5', 35 | 'Programming Language :: Python :: 3.6', 36 | 'Topic :: Communications', 37 | 'Topic :: Internet' 38 | ], 39 | entry_points={ 40 | 'hbmqtt.test.plugins': [ 41 | 'test_plugin = tests.plugins.test_manager:TestPlugin', 42 | 'event_plugin = tests.plugins.test_manager:EventTestPlugin', 43 | 'packet_logger_plugin = hbmqtt.plugins.logging:PacketLoggerPlugin', 44 | ], 45 | 'hbmqtt.broker.plugins': [ 46 | # 'event_logger_plugin = hbmqtt.plugins.logging:EventLoggerPlugin', 47 | 'packet_logger_plugin = hbmqtt.plugins.logging:PacketLoggerPlugin', 48 | 'auth_anonymous = hbmqtt.plugins.authentication:AnonymousAuthPlugin', 49 | 'auth_file = hbmqtt.plugins.authentication:FileAuthPlugin', 50 | 'topic_taboo = hbmqtt.plugins.topic_checking:TopicTabooPlugin', 51 | 'topic_acl = hbmqtt.plugins.topic_checking:TopicAccessControlListPlugin', 52 | 'broker_sys = hbmqtt.plugins.sys.broker:BrokerSysPlugin', 53 | ], 54 | 'hbmqtt.client.plugins': [ 55 | 'packet_logger_plugin = hbmqtt.plugins.logging:PacketLoggerPlugin', 56 | ], 57 | 'console_scripts': [ 58 | 'hbmqtt = scripts.broker_script:main', 59 | 'hbmqtt_pub = scripts.pub_script:main', 60 | 'hbmqtt_sub = scripts.sub_script:main', 61 | ] 62 | } 63 | ) 64 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | __author__ = 'nico' 2 | -------------------------------------------------------------------------------- /tests/mosquitto.org.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIC8DCCAlmgAwIBAgIJAOD63PlXjJi8MA0GCSqGSIb3DQEBBQUAMIGQMQswCQYD 3 | VQQGEwJHQjEXMBUGA1UECAwOVW5pdGVkIEtpbmdkb20xDjAMBgNVBAcMBURlcmJ5 4 | MRIwEAYDVQQKDAlNb3NxdWl0dG8xCzAJBgNVBAsMAkNBMRYwFAYDVQQDDA1tb3Nx 5 | dWl0dG8ub3JnMR8wHQYJKoZIhvcNAQkBFhByb2dlckBhdGNob28ub3JnMB4XDTEy 6 | MDYyOTIyMTE1OVoXDTIyMDYyNzIyMTE1OVowgZAxCzAJBgNVBAYTAkdCMRcwFQYD 7 | VQQIDA5Vbml0ZWQgS2luZ2RvbTEOMAwGA1UEBwwFRGVyYnkxEjAQBgNVBAoMCU1v 8 | c3F1aXR0bzELMAkGA1UECwwCQ0ExFjAUBgNVBAMMDW1vc3F1aXR0by5vcmcxHzAd 9 | BgkqhkiG9w0BCQEWEHJvZ2VyQGF0Y2hvby5vcmcwgZ8wDQYJKoZIhvcNAQEBBQAD 10 | gY0AMIGJAoGBAMYkLmX7SqOT/jJCZoQ1NWdCrr/pq47m3xxyXcI+FLEmwbE3R9vM 11 | rE6sRbP2S89pfrCt7iuITXPKycpUcIU0mtcT1OqxGBV2lb6RaOT2gC5pxyGaFJ+h 12 | A+GIbdYKO3JprPxSBoRponZJvDGEZuM3N7p3S/lRoi7G5wG5mvUmaE5RAgMBAAGj 13 | UDBOMB0GA1UdDgQWBBTad2QneVztIPQzRRGj6ZHKqJTv5jAfBgNVHSMEGDAWgBTa 14 | d2QneVztIPQzRRGj6ZHKqJTv5jAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBQUA 15 | A4GBAAqw1rK4NlRUCUBLhEFUQasjP7xfFqlVbE2cRy0Rs4o3KS0JwzQVBwG85xge 16 | REyPOFdGdhBY2P1FNRy0MDr6xr+D2ZOwxs63dG1nnAnWZg7qwoLgpZ4fESPD3PkA 17 | 1ZgKJc2zbSQ9fCPxt2W3mdVav66c6fsb7els2W2Iz7gERJSX 18 | -----END CERTIFICATE----- 19 | -------------------------------------------------------------------------------- /tests/mqtt/__init__.py: -------------------------------------------------------------------------------- 1 | __author__ = 'nico' 2 | -------------------------------------------------------------------------------- /tests/mqtt/protocol/__init__.py: -------------------------------------------------------------------------------- 1 | __author__ = 'nico' 2 | -------------------------------------------------------------------------------- /tests/mqtt/test_connect.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2015 Nicolas JOUANIN 2 | # 3 | # See the file license.txt for copying permission. 4 | import unittest 5 | import asyncio 6 | 7 | from hbmqtt.mqtt.connect import ConnectPacket, ConnectVariableHeader, ConnectPayload 8 | from hbmqtt.mqtt.packet import MQTTFixedHeader, CONNECT 9 | from hbmqtt.adapters import BufferReader 10 | 11 | 12 | class ConnectPacketTest(unittest.TestCase): 13 | def setUp(self): 14 | self.loop = asyncio.new_event_loop() 15 | 16 | def test_decode_ok(self): 17 | data = b'\x10\x3e\x00\x04MQTT\x04\xce\x00\x00\x00\x0a0123456789\x00\x09WillTopic\x00\x0bWillMessage\x00\x04user\x00\x08password' 18 | stream = BufferReader(data) 19 | message = self.loop.run_until_complete(ConnectPacket.from_stream(stream)) 20 | self.assertEqual(message.variable_header.proto_name, "MQTT") 21 | self.assertEqual(message.variable_header.proto_level, 4) 22 | self.assertTrue(message.variable_header.username_flag) 23 | self.assertTrue(message.variable_header.password_flag) 24 | self.assertFalse(message.variable_header.will_retain_flag) 25 | self.assertEqual(message.variable_header.will_qos, 1) 26 | self.assertTrue(message.variable_header.will_flag) 27 | self.assertTrue(message.variable_header.clean_session_flag) 28 | self.assertFalse(message.variable_header.reserved_flag) 29 | self.assertEqual(message.payload.client_id, '0123456789') 30 | self.assertEqual(message.payload.will_topic, 'WillTopic') 31 | self.assertEqual(message.payload.will_message, b'WillMessage') 32 | self.assertEqual(message.payload.username, 'user') 33 | self.assertEqual(message.payload.password, 'password') 34 | 35 | def test_decode_ok_will_flag(self): 36 | data = b'\x10\x26\x00\x04MQTT\x04\xca\x00\x00\x00\x0a0123456789\x00\x04user\x00\x08password' 37 | stream = BufferReader(data) 38 | message = self.loop.run_until_complete(ConnectPacket.from_stream(stream)) 39 | self.assertEqual(message.variable_header.proto_name, "MQTT") 40 | self.assertEqual(message.variable_header.proto_level, 4) 41 | self.assertTrue(message.variable_header.username_flag) 42 | self.assertTrue(message.variable_header.password_flag) 43 | self.assertFalse(message.variable_header.will_retain_flag) 44 | self.assertEqual(message.variable_header.will_qos, 1) 45 | self.assertFalse(message.variable_header.will_flag) 46 | self.assertTrue(message.variable_header.clean_session_flag) 47 | self.assertFalse(message.variable_header.reserved_flag) 48 | self.assertEqual(message.payload.client_id, '0123456789') 49 | self.assertEqual(message.payload.will_topic, None) 50 | self.assertEqual(message.payload.will_message, None) 51 | self.assertEqual(message.payload.username, 'user') 52 | self.assertEqual(message.payload.password, 'password') 53 | 54 | def test_decode_fail_reserved_flag(self): 55 | data = b'\x10\x3e\x00\x04MQTT\x04\xcf\x00\x00\x00\x0a0123456789\x00\x09WillTopic\x00\x0bWillMessage\x00\x04user\x00\x08password' 56 | stream = BufferReader(data) 57 | message = self.loop.run_until_complete(ConnectPacket.from_stream(stream)) 58 | self.assertTrue(message.variable_header.reserved_flag) 59 | 60 | def test_decode_fail_miss_clientId(self): 61 | data = b'\x10\x0a\x00\x04MQTT\x04\xce\x00\x00' 62 | stream = BufferReader(data) 63 | message = self.loop.run_until_complete(ConnectPacket.from_stream(stream)) 64 | self.assertIsNot(message.payload.client_id, None) 65 | 66 | def test_decode_fail_miss_willtopic(self): 67 | data = b'\x10\x16\x00\x04MQTT\x04\xce\x00\x00\x00\x0a0123456789' 68 | stream = BufferReader(data) 69 | message = self.loop.run_until_complete(ConnectPacket.from_stream(stream)) 70 | self.assertIs(message.payload.will_topic, None) 71 | 72 | def test_decode_fail_miss_username(self): 73 | data = b'\x10\x2e\x00\x04MQTT\x04\xce\x00\x00\x00\x0a0123456789\x00\x09WillTopic\x00\x0bWillMessage' 74 | stream = BufferReader(data) 75 | message = self.loop.run_until_complete(ConnectPacket.from_stream(stream)) 76 | self.assertIs(message.payload.username, None) 77 | 78 | def test_decode_fail_miss_password(self): 79 | data = b'\x10\x34\x00\x04MQTT\x04\xce\x00\x00\x00\x0a0123456789\x00\x09WillTopic\x00\x0bWillMessage\x00\x04user' 80 | stream = BufferReader(data) 81 | message = self.loop.run_until_complete(ConnectPacket.from_stream(stream)) 82 | self.assertIs(message.payload.password, None) 83 | 84 | def test_encode(self): 85 | header = MQTTFixedHeader(CONNECT, 0x00, 0) 86 | variable_header = ConnectVariableHeader(0xce, 0, 'MQTT', 4) 87 | payload = ConnectPayload('0123456789', 'WillTopic', b'WillMessage', 'user', 'password') 88 | message = ConnectPacket(header, variable_header, payload) 89 | encoded = message.to_bytes() 90 | self.assertEqual(encoded, b'\x10\x3e\x00\x04MQTT\x04\xce\x00\x00\x00\x0a0123456789\x00\x09WillTopic\x00\x0bWillMessage\x00\x04user\x00\x08password') 91 | 92 | def test_getattr_ok(self): 93 | data = b'\x10\x3e\x00\x04MQTT\x04\xce\x00\x00\x00\x0a0123456789\x00\x09WillTopic\x00\x0bWillMessage\x00\x04user\x00\x08password' 94 | stream = BufferReader(data) 95 | message = self.loop.run_until_complete(ConnectPacket.from_stream(stream)) 96 | self.assertEqual(message.variable_header.proto_name, "MQTT") 97 | self.assertEqual(message.proto_name, "MQTT") 98 | self.assertEqual(message.variable_header.proto_level, 4) 99 | self.assertEqual(message.proto_level, 4) 100 | self.assertTrue(message.variable_header.username_flag) 101 | self.assertTrue(message.username_flag) 102 | self.assertTrue(message.variable_header.password_flag) 103 | self.assertTrue(message.password_flag) 104 | self.assertFalse(message.variable_header.will_retain_flag) 105 | self.assertFalse(message.will_retain_flag) 106 | self.assertEqual(message.variable_header.will_qos, 1) 107 | self.assertEqual(message.will_qos, 1) 108 | self.assertTrue(message.variable_header.will_flag) 109 | self.assertTrue(message.will_flag) 110 | self.assertTrue(message.variable_header.clean_session_flag) 111 | self.assertTrue(message.clean_session_flag) 112 | self.assertFalse(message.variable_header.reserved_flag) 113 | self.assertFalse(message.reserved_flag) 114 | self.assertEqual(message.payload.client_id, '0123456789') 115 | self.assertEqual(message.client_id, '0123456789') 116 | self.assertEqual(message.payload.will_topic, 'WillTopic') 117 | self.assertEqual(message.will_topic, 'WillTopic') 118 | self.assertEqual(message.payload.will_message, b'WillMessage') 119 | self.assertEqual(message.will_message, b'WillMessage') 120 | self.assertEqual(message.payload.username, 'user') 121 | self.assertEqual(message.username, 'user') 122 | self.assertEqual(message.payload.password, 'password') 123 | self.assertEqual(message.password, 'password') 124 | -------------------------------------------------------------------------------- /tests/mqtt/test_packet.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2015 Nicolas JOUANIN 2 | # 3 | # See the file license.txt for copying permission. 4 | import unittest 5 | import asyncio 6 | 7 | from hbmqtt.mqtt.packet import CONNECT, MQTTFixedHeader 8 | from hbmqtt.errors import MQTTException 9 | from hbmqtt.adapters import BufferReader 10 | 11 | 12 | class TestMQTTFixedHeaderTest(unittest.TestCase): 13 | def setUp(self): 14 | self.loop = asyncio.new_event_loop() 15 | 16 | def test_from_bytes(self): 17 | data = b'\x10\x7f' 18 | stream = BufferReader(data) 19 | header = self.loop.run_until_complete(MQTTFixedHeader.from_stream(stream)) 20 | self.assertEqual(header.packet_type, CONNECT) 21 | self.assertFalse(header.flags & 0x08) 22 | self.assertEqual((header.flags & 0x06) >> 1, 0) 23 | self.assertFalse(header.flags & 0x01) 24 | self.assertEqual(header.remaining_length, 127) 25 | 26 | def test_from_bytes_with_length(self): 27 | data = b'\x10\xff\xff\xff\x7f' 28 | stream = BufferReader(data) 29 | header = self.loop.run_until_complete(MQTTFixedHeader.from_stream(stream)) 30 | self.assertEqual(header.packet_type, CONNECT) 31 | self.assertFalse(header.flags & 0x08) 32 | self.assertEqual((header.flags & 0x06) >> 1, 0) 33 | self.assertFalse(header.flags & 0x01) 34 | self.assertEqual(header.remaining_length, 268435455) 35 | 36 | def test_from_bytes_ko_with_length(self): 37 | data = b'\x10\xff\xff\xff\xff\x7f' 38 | stream = BufferReader(data) 39 | with self.assertRaises(MQTTException): 40 | self.loop.run_until_complete(MQTTFixedHeader.from_stream(stream)) 41 | 42 | def test_to_bytes(self): 43 | header = MQTTFixedHeader(CONNECT, 0x00, 0) 44 | data = header.to_bytes() 45 | self.assertEqual(data, b'\x10\x00') 46 | 47 | def test_to_bytes_2(self): 48 | header = MQTTFixedHeader(CONNECT, 0x00, 268435455) 49 | data = header.to_bytes() 50 | self.assertEqual(data, b'\x10\xff\xff\xff\x7f') 51 | -------------------------------------------------------------------------------- /tests/mqtt/test_puback.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2015 Nicolas JOUANIN 2 | # 3 | # See the file license.txt for copying permission. 4 | import asyncio 5 | import unittest 6 | 7 | from hbmqtt.mqtt.puback import PubackPacket, PacketIdVariableHeader 8 | from hbmqtt.adapters import BufferReader 9 | 10 | 11 | class PubackPacketTest(unittest.TestCase): 12 | def setUp(self): 13 | self.loop = asyncio.new_event_loop() 14 | 15 | def test_from_stream(self): 16 | data = b'\x40\x02\x00\x0a' 17 | stream = BufferReader(data) 18 | message = self.loop.run_until_complete(PubackPacket.from_stream(stream)) 19 | self.assertEqual(message.variable_header.packet_id, 10) 20 | 21 | def test_to_bytes(self): 22 | variable_header = PacketIdVariableHeader(10) 23 | publish = PubackPacket(variable_header=variable_header) 24 | out = publish.to_bytes() 25 | self.assertEqual(out, b'\x40\x02\x00\x0a') 26 | -------------------------------------------------------------------------------- /tests/mqtt/test_pubcomp.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2015 Nicolas JOUANIN 2 | # 3 | # See the file license.txt for copying permission. 4 | import asyncio 5 | import unittest 6 | 7 | from hbmqtt.mqtt.pubcomp import PubcompPacket, PacketIdVariableHeader 8 | from hbmqtt.adapters import BufferReader 9 | 10 | 11 | class PubcompPacketTest(unittest.TestCase): 12 | def setUp(self): 13 | self.loop = asyncio.new_event_loop() 14 | 15 | def test_from_stream(self): 16 | data = b'\x70\x02\x00\x0a' 17 | stream = BufferReader(data) 18 | message = self.loop.run_until_complete(PubcompPacket.from_stream(stream)) 19 | self.assertEqual(message.variable_header.packet_id, 10) 20 | 21 | def test_to_bytes(self): 22 | variable_header = PacketIdVariableHeader(10) 23 | publish = PubcompPacket(variable_header=variable_header) 24 | out = publish.to_bytes() 25 | self.assertEqual(out, b'\x70\x02\x00\x0a') 26 | -------------------------------------------------------------------------------- /tests/mqtt/test_publish.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2015 Nicolas JOUANIN 2 | # 3 | # See the file license.txt for copying permission. 4 | import asyncio 5 | import unittest 6 | 7 | from hbmqtt.mqtt.publish import PublishPacket, PublishVariableHeader, PublishPayload 8 | from hbmqtt.adapters import BufferReader 9 | from hbmqtt.mqtt.constants import QOS_0, QOS_1, QOS_2 10 | 11 | 12 | class PublishPacketTest(unittest.TestCase): 13 | def setUp(self): 14 | self.loop = asyncio.new_event_loop() 15 | 16 | def test_from_stream_qos_0(self): 17 | data = b'\x31\x11\x00\x05topic0123456789' 18 | stream = BufferReader(data) 19 | message = self.loop.run_until_complete(PublishPacket.from_stream(stream)) 20 | self.assertEqual(message.variable_header.topic_name, 'topic') 21 | self.assertEqual(message.variable_header.packet_id, None) 22 | self.assertFalse((message.fixed_header.flags >> 1) & 0x03) 23 | self.assertTrue(message.fixed_header.flags & 0x01) 24 | self.assertTrue(message.payload.data, b'0123456789') 25 | 26 | def test_from_stream_qos_2(self): 27 | data = b'\x37\x13\x00\x05topic\x00\x0a0123456789' 28 | stream = BufferReader(data) 29 | message = self.loop.run_until_complete(PublishPacket.from_stream(stream)) 30 | self.assertEqual(message.variable_header.topic_name, 'topic') 31 | self.assertEqual(message.variable_header.packet_id, 10) 32 | self.assertTrue((message.fixed_header.flags >> 1) & 0x03) 33 | self.assertTrue(message.fixed_header.flags & 0x01) 34 | self.assertTrue(message.payload.data, b'0123456789') 35 | 36 | def test_to_stream_no_packet_id(self): 37 | variable_header = PublishVariableHeader('topic', None) 38 | payload = PublishPayload(b'0123456789') 39 | publish = PublishPacket(variable_header=variable_header, payload=payload) 40 | out = publish.to_bytes() 41 | self.assertEqual(out, b'\x30\x11\x00\x05topic0123456789') 42 | 43 | def test_to_stream_packet(self): 44 | variable_header = PublishVariableHeader('topic', 10) 45 | payload = PublishPayload(b'0123456789') 46 | publish = PublishPacket(variable_header=variable_header, payload=payload) 47 | out = publish.to_bytes() 48 | self.assertEqual(out, b'\x30\x13\x00\x05topic\00\x0a0123456789') 49 | 50 | def test_build(self): 51 | packet = PublishPacket.build('/topic', b'data', 1, False, QOS_0, False) 52 | self.assertEqual(packet.packet_id, 1) 53 | self.assertFalse(packet.dup_flag) 54 | self.assertEqual(packet.qos, QOS_0) 55 | self.assertFalse(packet.retain_flag) 56 | 57 | packet = PublishPacket.build('/topic', b'data', 1, False, QOS_1, False) 58 | self.assertEqual(packet.packet_id, 1) 59 | self.assertFalse(packet.dup_flag) 60 | self.assertEqual(packet.qos, QOS_1) 61 | self.assertFalse(packet.retain_flag) 62 | 63 | packet = PublishPacket.build('/topic', b'data', 1, False, QOS_2, False) 64 | self.assertEqual(packet.packet_id, 1) 65 | self.assertFalse(packet.dup_flag) 66 | self.assertEqual(packet.qos, QOS_2) 67 | self.assertFalse(packet.retain_flag) 68 | 69 | packet = PublishPacket.build('/topic', b'data', 1, True, QOS_0, False) 70 | self.assertEqual(packet.packet_id, 1) 71 | self.assertTrue(packet.dup_flag) 72 | self.assertEqual(packet.qos, QOS_0) 73 | self.assertFalse(packet.retain_flag) 74 | 75 | packet = PublishPacket.build('/topic', b'data', 1, True, QOS_1, False) 76 | self.assertEqual(packet.packet_id, 1) 77 | self.assertTrue(packet.dup_flag) 78 | self.assertEqual(packet.qos, QOS_1) 79 | self.assertFalse(packet.retain_flag) 80 | 81 | packet = PublishPacket.build('/topic', b'data', 1, True, QOS_2, False) 82 | self.assertEqual(packet.packet_id, 1) 83 | self.assertTrue(packet.dup_flag) 84 | self.assertEqual(packet.qos, QOS_2) 85 | self.assertFalse(packet.retain_flag) 86 | 87 | packet = PublishPacket.build('/topic', b'data', 1, False, QOS_0, True) 88 | self.assertEqual(packet.packet_id, 1) 89 | self.assertFalse(packet.dup_flag) 90 | self.assertEqual(packet.qos, QOS_0) 91 | self.assertTrue(packet.retain_flag) 92 | 93 | packet = PublishPacket.build('/topic', b'data', 1, False, QOS_1, True) 94 | self.assertEqual(packet.packet_id, 1) 95 | self.assertFalse(packet.dup_flag) 96 | self.assertEqual(packet.qos, QOS_1) 97 | self.assertTrue(packet.retain_flag) 98 | 99 | packet = PublishPacket.build('/topic', b'data', 1, False, QOS_2, True) 100 | self.assertEqual(packet.packet_id, 1) 101 | self.assertFalse(packet.dup_flag) 102 | self.assertEqual(packet.qos, QOS_2) 103 | self.assertTrue(packet.retain_flag) 104 | 105 | packet = PublishPacket.build('/topic', b'data', 1, True, QOS_0, True) 106 | self.assertEqual(packet.packet_id, 1) 107 | self.assertTrue(packet.dup_flag) 108 | self.assertEqual(packet.qos, QOS_0) 109 | self.assertTrue(packet.retain_flag) 110 | 111 | packet = PublishPacket.build('/topic', b'data', 1, True, QOS_1, True) 112 | self.assertEqual(packet.packet_id, 1) 113 | self.assertTrue(packet.dup_flag) 114 | self.assertEqual(packet.qos, QOS_1) 115 | self.assertTrue(packet.retain_flag) 116 | 117 | packet = PublishPacket.build('/topic', b'data', 1, True, QOS_2, True) 118 | self.assertEqual(packet.packet_id, 1) 119 | self.assertTrue(packet.dup_flag) 120 | self.assertEqual(packet.qos, QOS_2) 121 | self.assertTrue(packet.retain_flag) 122 | -------------------------------------------------------------------------------- /tests/mqtt/test_pubrec.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2015 Nicolas JOUANIN 2 | # 3 | # See the file license.txt for copying permission. 4 | import asyncio 5 | import unittest 6 | 7 | from hbmqtt.mqtt.pubrec import PubrecPacket, PacketIdVariableHeader 8 | from hbmqtt.adapters import BufferReader 9 | 10 | 11 | class PubrecPacketTest(unittest.TestCase): 12 | def setUp(self): 13 | self.loop = asyncio.new_event_loop() 14 | 15 | def test_from_stream(self): 16 | data = b'\x50\x02\x00\x0a' 17 | stream = BufferReader(data) 18 | message = self.loop.run_until_complete(PubrecPacket.from_stream(stream)) 19 | self.assertEqual(message.variable_header.packet_id, 10) 20 | 21 | def test_to_bytes(self): 22 | variable_header = PacketIdVariableHeader(10) 23 | publish = PubrecPacket(variable_header=variable_header) 24 | out = publish.to_bytes() 25 | self.assertEqual(out, b'\x50\x02\x00\x0a') 26 | -------------------------------------------------------------------------------- /tests/mqtt/test_pubrel.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2015 Nicolas JOUANIN 2 | # 3 | # See the file license.txt for copying permission. 4 | import asyncio 5 | import unittest 6 | 7 | from hbmqtt.mqtt.pubrel import PubrelPacket, PacketIdVariableHeader 8 | from hbmqtt.adapters import BufferReader 9 | 10 | 11 | class PubrelPacketTest(unittest.TestCase): 12 | def setUp(self): 13 | self.loop = asyncio.new_event_loop() 14 | 15 | def test_from_stream(self): 16 | data = b'\x60\x02\x00\x0a' 17 | stream = BufferReader(data) 18 | message = self.loop.run_until_complete(PubrelPacket.from_stream(stream)) 19 | self.assertEqual(message.variable_header.packet_id, 10) 20 | 21 | def test_to_bytes(self): 22 | variable_header = PacketIdVariableHeader(10) 23 | publish = PubrelPacket(variable_header=variable_header) 24 | out = publish.to_bytes() 25 | self.assertEqual(out, b'\x62\x02\x00\x0a') 26 | -------------------------------------------------------------------------------- /tests/mqtt/test_suback.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2015 Nicolas JOUANIN 2 | # 3 | # See the file license.txt for copying permission. 4 | import asyncio 5 | import unittest 6 | 7 | from hbmqtt.mqtt.suback import SubackPacket, SubackPayload 8 | from hbmqtt.mqtt.packet import PacketIdVariableHeader 9 | from hbmqtt.adapters import BufferReader 10 | 11 | 12 | class SubackPacketTest(unittest.TestCase): 13 | def setUp(self): 14 | self.loop = asyncio.new_event_loop() 15 | 16 | def test_from_stream(self): 17 | data = b'\x90\x06\x00\x0a\x00\x01\x02\x80' 18 | stream = BufferReader(data) 19 | message = self.loop.run_until_complete(SubackPacket.from_stream(stream)) 20 | self.assertEqual(message.payload.return_codes[0], SubackPayload.RETURN_CODE_00) 21 | self.assertEqual(message.payload.return_codes[1], SubackPayload.RETURN_CODE_01) 22 | self.assertEqual(message.payload.return_codes[2], SubackPayload.RETURN_CODE_02) 23 | self.assertEqual(message.payload.return_codes[3], SubackPayload.RETURN_CODE_80) 24 | 25 | def test_to_stream(self): 26 | variable_header = PacketIdVariableHeader(10) 27 | payload = SubackPayload( 28 | [SubackPayload.RETURN_CODE_00, 29 | SubackPayload.RETURN_CODE_01, 30 | SubackPayload.RETURN_CODE_02, 31 | SubackPayload.RETURN_CODE_80 32 | ]) 33 | suback = SubackPacket(variable_header=variable_header, payload=payload) 34 | out = suback.to_bytes() 35 | self.assertEqual(out, b'\x90\x06\x00\x0a\x00\x01\x02\x80') 36 | -------------------------------------------------------------------------------- /tests/mqtt/test_subscribe.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2015 Nicolas JOUANIN 2 | # 3 | # See the file license.txt for copying permission. 4 | import asyncio 5 | import unittest 6 | 7 | from hbmqtt.mqtt.subscribe import SubscribePacket, SubscribePayload 8 | from hbmqtt.mqtt.packet import PacketIdVariableHeader 9 | from hbmqtt.mqtt.constants import QOS_1, QOS_2 10 | from hbmqtt.adapters import BufferReader 11 | 12 | 13 | class SubscribePacketTest(unittest.TestCase): 14 | def setUp(self): 15 | self.loop = asyncio.new_event_loop() 16 | 17 | def test_from_stream(self): 18 | data = b'\x80\x0e\x00\x0a\x00\x03a/b\x01\x00\x03c/d\x02' 19 | stream = BufferReader(data) 20 | message = self.loop.run_until_complete(SubscribePacket.from_stream(stream)) 21 | (topic, qos) = message.payload.topics[0] 22 | self.assertEqual(topic, 'a/b') 23 | self.assertEqual(qos, QOS_1) 24 | (topic, qos) = message.payload.topics[1] 25 | self.assertEqual(topic, 'c/d') 26 | self.assertEqual(qos, QOS_2) 27 | 28 | def test_to_stream(self): 29 | variable_header = PacketIdVariableHeader(10) 30 | payload = SubscribePayload( 31 | [ 32 | ('a/b', QOS_1), 33 | ('c/d', QOS_2) 34 | ]) 35 | publish = SubscribePacket(variable_header=variable_header, payload=payload) 36 | out = publish.to_bytes() 37 | self.assertEqual(out, b'\x82\x0e\x00\x0a\x00\x03a/b\x01\x00\x03c/d\x02') 38 | -------------------------------------------------------------------------------- /tests/mqtt/test_unsuback.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2015 Nicolas JOUANIN 2 | # 3 | # See the file license.txt for copying permission. 4 | import asyncio 5 | import unittest 6 | 7 | from hbmqtt.mqtt.unsuback import UnsubackPacket 8 | from hbmqtt.mqtt.packet import PacketIdVariableHeader 9 | from hbmqtt.adapters import BufferReader 10 | 11 | 12 | class UnsubackPacketTest(unittest.TestCase): 13 | def setUp(self): 14 | self.loop = asyncio.new_event_loop() 15 | 16 | def test_from_stream(self): 17 | data = b'\xb0\x02\x00\x0a' 18 | stream = BufferReader(data) 19 | message = self.loop.run_until_complete(UnsubackPacket.from_stream(stream)) 20 | self.assertEqual(message.variable_header.packet_id, 10) 21 | 22 | def test_to_stream(self): 23 | variable_header = PacketIdVariableHeader(10) 24 | publish = UnsubackPacket(variable_header=variable_header) 25 | out = publish.to_bytes() 26 | self.assertEqual(out, b'\xb0\x02\x00\x0a') 27 | -------------------------------------------------------------------------------- /tests/mqtt/test_unsubscribe.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2015 Nicolas JOUANIN 2 | # 3 | # See the file license.txt for copying permission. 4 | import asyncio 5 | import unittest 6 | 7 | from hbmqtt.mqtt.unsubscribe import UnsubscribePacket, UnubscribePayload 8 | from hbmqtt.mqtt.packet import PacketIdVariableHeader 9 | from hbmqtt.adapters import BufferReader 10 | 11 | 12 | class UnsubscribePacketTest(unittest.TestCase): 13 | def setUp(self): 14 | self.loop = asyncio.new_event_loop() 15 | 16 | def test_from_stream(self): 17 | data = b'\xa2\x0c\x00\n\x00\x03a/b\x00\x03c/d' 18 | stream = BufferReader(data) 19 | message = self.loop.run_until_complete(UnsubscribePacket.from_stream(stream)) 20 | self.assertEqual(message.payload.topics[0], 'a/b') 21 | self.assertEqual(message.payload.topics[1], 'c/d') 22 | 23 | def test_to_stream(self): 24 | variable_header = PacketIdVariableHeader(10) 25 | payload = UnubscribePayload(['a/b', 'c/d']) 26 | publish = UnsubscribePacket(variable_header=variable_header, payload=payload) 27 | out = publish.to_bytes() 28 | self.assertEqual(out, b'\xa2\x0c\x00\n\x00\x03a/b\x00\x03c/d') 29 | -------------------------------------------------------------------------------- /tests/plugins/__init__.py: -------------------------------------------------------------------------------- 1 | __author__ = 'nico' 2 | -------------------------------------------------------------------------------- /tests/plugins/passwd: -------------------------------------------------------------------------------- 1 | # Test password file 2 | user:$6$1sSXVMBVKMF$uDStq59YfiuFxVF1Gi/.i/Li7Vwf5iTwg8LovLKmCvM5FsRNJM.OPWHhXwI2.4AscLZXSPQVxpIlX6laUl9570 3 | test_user:$6$.c9f9sAzs5YXX2de$GSdOi3iFwHJRCIJn1W63muDFQAL29yoFmU/TXcwDB42F2BZg3zcN5uKBM0.1PjwdMpWHRydbhXWSc3uWKSmKr. -------------------------------------------------------------------------------- /tests/plugins/test_authentication.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2015 Nicolas JOUANIN 2 | # 3 | # See the file license.txt for copying permission. 4 | 5 | import unittest 6 | import logging 7 | import os 8 | import asyncio 9 | from hbmqtt.plugins.manager import BaseContext 10 | from hbmqtt.plugins.authentication import AnonymousAuthPlugin, FileAuthPlugin 11 | from hbmqtt.session import Session 12 | 13 | formatter = "[%(asctime)s] %(name)s {%(filename)s:%(lineno)d} %(levelname)s - %(message)s" 14 | logging.basicConfig(level=logging.DEBUG, format=formatter) 15 | 16 | 17 | class TestAnonymousAuthPlugin(unittest.TestCase): 18 | def setUp(self): 19 | self.loop = asyncio.new_event_loop() 20 | 21 | def test_allow_anonymous(self): 22 | context = BaseContext() 23 | context.logger = logging.getLogger(__name__) 24 | context.config = { 25 | 'auth': { 26 | 'allow-anonymous': True 27 | } 28 | } 29 | s = Session() 30 | s.username = "" 31 | auth_plugin = AnonymousAuthPlugin(context) 32 | ret = self.loop.run_until_complete(auth_plugin.authenticate(session=s)) 33 | self.assertTrue(ret) 34 | 35 | def test_disallow_anonymous(self): 36 | context = BaseContext() 37 | context.logger = logging.getLogger(__name__) 38 | context.config = { 39 | 'auth': { 40 | 'allow-anonymous': False 41 | } 42 | } 43 | s = Session() 44 | s.username = "" 45 | auth_plugin = AnonymousAuthPlugin(context) 46 | ret = self.loop.run_until_complete(auth_plugin.authenticate(session=s)) 47 | self.assertFalse(ret) 48 | 49 | def test_allow_nonanonymous(self): 50 | context = BaseContext() 51 | context.logger = logging.getLogger(__name__) 52 | context.config = { 53 | 'auth': { 54 | 'allow-anonymous': False 55 | } 56 | } 57 | s = Session() 58 | s.username = "test" 59 | auth_plugin = AnonymousAuthPlugin(context) 60 | ret = self.loop.run_until_complete(auth_plugin.authenticate(session=s)) 61 | self.assertTrue(ret) 62 | 63 | 64 | class TestFileAuthPlugin(unittest.TestCase): 65 | def setUp(self): 66 | self.loop = asyncio.new_event_loop() 67 | 68 | def test_allow(self): 69 | context = BaseContext() 70 | context.logger = logging.getLogger(__name__) 71 | context.config = { 72 | 'auth': { 73 | 'password-file': os.path.join(os.path.dirname(os.path.realpath(__file__)), "passwd") 74 | } 75 | } 76 | s = Session() 77 | s.username = "user" 78 | s.password = "test" 79 | auth_plugin = FileAuthPlugin(context) 80 | ret = self.loop.run_until_complete(auth_plugin.authenticate(session=s)) 81 | self.assertTrue(ret) 82 | 83 | def test_wrong_password(self): 84 | context = BaseContext() 85 | context.logger = logging.getLogger(__name__) 86 | context.config = { 87 | 'auth': { 88 | 'password-file': os.path.join(os.path.dirname(os.path.realpath(__file__)), "passwd") 89 | } 90 | } 91 | s = Session() 92 | s.username = "user" 93 | s.password = "wrong password" 94 | auth_plugin = FileAuthPlugin(context) 95 | ret = self.loop.run_until_complete(auth_plugin.authenticate(session=s)) 96 | self.assertFalse(ret) 97 | 98 | def test_unknown_password(self): 99 | context = BaseContext() 100 | context.logger = logging.getLogger(__name__) 101 | context.config = { 102 | 'auth': { 103 | 'password-file': os.path.join(os.path.dirname(os.path.realpath(__file__)), "passwd") 104 | } 105 | } 106 | s = Session() 107 | s.username = "some user" 108 | s.password = "some password" 109 | auth_plugin = FileAuthPlugin(context) 110 | ret = self.loop.run_until_complete(auth_plugin.authenticate(session=s)) 111 | self.assertFalse(ret) 112 | -------------------------------------------------------------------------------- /tests/plugins/test_manager.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2015 Nicolas JOUANIN 2 | # 3 | # See the file license.txt for copying permission. 4 | import unittest 5 | import logging 6 | import asyncio 7 | from hbmqtt.plugins.manager import PluginManager 8 | 9 | formatter = "[%(asctime)s] %(name)s {%(filename)s:%(lineno)d} %(levelname)s - %(message)s" 10 | logging.basicConfig(level=logging.INFO, format=formatter) 11 | 12 | 13 | class TestPlugin: 14 | def __init__(self, context): 15 | self.context = context 16 | 17 | 18 | class EventTestPlugin: 19 | def __init__(self, context): 20 | self.context = context 21 | self.test_flag = False 22 | self.coro_flag = False 23 | 24 | @asyncio.coroutine 25 | def on_test(self, *args, **kwargs): 26 | self.test_flag = True 27 | self.context.logger.info("on_test") 28 | 29 | @asyncio.coroutine 30 | def test_coro(self, *args, **kwargs): 31 | self.coro_flag = True 32 | 33 | @asyncio.coroutine 34 | def ret_coro(self, *args, **kwargs): 35 | return "TEST" 36 | 37 | 38 | class TestPluginManager(unittest.TestCase): 39 | def setUp(self): 40 | self.loop = asyncio.new_event_loop() 41 | 42 | def test_load_plugin(self): 43 | manager = PluginManager("hbmqtt.test.plugins", context=None) 44 | self.assertTrue(len(manager._plugins) > 0) 45 | 46 | def test_fire_event(self): 47 | @asyncio.coroutine 48 | def fire_event(): 49 | yield from manager.fire_event("test") 50 | yield from asyncio.sleep(1, loop=self.loop) 51 | yield from manager.close() 52 | 53 | manager = PluginManager("hbmqtt.test.plugins", context=None, loop=self.loop) 54 | self.loop.run_until_complete(fire_event()) 55 | plugin = manager.get_plugin("event_plugin") 56 | self.assertTrue(plugin.object.test_flag) 57 | 58 | def test_fire_event_wait(self): 59 | @asyncio.coroutine 60 | def fire_event(): 61 | yield from manager.fire_event("test", wait=True) 62 | yield from manager.close() 63 | 64 | manager = PluginManager("hbmqtt.test.plugins", context=None, loop=self.loop) 65 | self.loop.run_until_complete(fire_event()) 66 | plugin = manager.get_plugin("event_plugin") 67 | self.assertTrue(plugin.object.test_flag) 68 | 69 | def test_map_coro(self): 70 | @asyncio.coroutine 71 | def call_coro(): 72 | yield from manager.map_plugin_coro('test_coro') 73 | 74 | manager = PluginManager("hbmqtt.test.plugins", context=None, loop=self.loop) 75 | self.loop.run_until_complete(call_coro()) 76 | plugin = manager.get_plugin("event_plugin") 77 | self.assertTrue(plugin.object.test_coro) 78 | 79 | def test_map_coro_return(self): 80 | @asyncio.coroutine 81 | def call_coro(): 82 | return (yield from manager.map_plugin_coro('ret_coro')) 83 | 84 | manager = PluginManager("hbmqtt.test.plugins", context=None, loop=self.loop) 85 | ret = self.loop.run_until_complete(call_coro()) 86 | plugin = manager.get_plugin("event_plugin") 87 | self.assertEqual(ret[plugin], "TEST") 88 | 89 | def test_map_coro_filter(self): 90 | """ 91 | Run plugin coro but expect no return as an empty filter is given 92 | :return: 93 | """ 94 | @asyncio.coroutine 95 | def call_coro(): 96 | return (yield from manager.map_plugin_coro('ret_coro', filter_plugins=[])) 97 | 98 | manager = PluginManager("hbmqtt.test.plugins", context=None, loop=self.loop) 99 | ret = self.loop.run_until_complete(call_coro()) 100 | self.assertTrue(len(ret) == 0) 101 | -------------------------------------------------------------------------------- /tests/plugins/test_persistence.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2015 Nicolas JOUANIN 2 | # 3 | # See the file license.txt for copying permission. 4 | 5 | import unittest 6 | import logging 7 | import os 8 | import asyncio 9 | import sqlite3 10 | from hbmqtt.plugins.manager import BaseContext 11 | from hbmqtt.plugins.persistence import SQLitePlugin 12 | 13 | formatter = "[%(asctime)s] %(name)s {%(filename)s:%(lineno)d} %(levelname)s - %(message)s" 14 | logging.basicConfig(level=logging.DEBUG, format=formatter) 15 | 16 | 17 | class TestSQLitePlugin(unittest.TestCase): 18 | def setUp(self): 19 | self.loop = asyncio.new_event_loop() 20 | 21 | def test_create_tables(self): 22 | dbfile = os.path.join(os.path.dirname(os.path.realpath(__file__)), "test.db") 23 | context = BaseContext() 24 | context.logger = logging.getLogger(__name__) 25 | context.config = { 26 | 'persistence': { 27 | 'file': dbfile 28 | } 29 | } 30 | SQLitePlugin(context) 31 | 32 | conn = sqlite3.connect(dbfile) 33 | cursor = conn.cursor() 34 | rows = cursor.execute("SELECT name FROM sqlite_master where type = 'table'") 35 | tables = [] 36 | for row in rows: 37 | tables.append(row[0]) 38 | self.assertIn("session", tables) 39 | 40 | # def test_save_session(self): 41 | # dbfile = os.path.join(os.path.dirname(os.path.realpath(__file__)), "test.db") 42 | # context = BaseContext() 43 | # context.logger = logging.getLogger(__name__) 44 | # context.config = { 45 | # 'persistence': { 46 | # 'file': dbfile 47 | # } 48 | # } 49 | # sql_plugin = SQLitePlugin(context) 50 | # s = Session() 51 | # s.client_id = 'test_save_session' 52 | # ret = self.loop.run_until_complete(sql_plugin.save_session(session=s)) 53 | # 54 | # conn = sqlite3.connect(dbfile) 55 | # cursor = conn.cursor() 56 | # row = cursor.execute("SELECT client_id FROM session where client_id = 'test_save_session'").fetchone() 57 | # self.assertTrue(len(row) == 1) 58 | # self.assertEqual(row[0], s.client_id) 59 | -------------------------------------------------------------------------------- /tests/test_codecs.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2015 Nicolas JOUANIN 2 | # 3 | # See the file license.txt for copying permission. 4 | import unittest 5 | import asyncio 6 | 7 | from hbmqtt.codecs import ( 8 | bytes_to_hex_str, 9 | bytes_to_int, 10 | decode_string, 11 | encode_string, 12 | ) 13 | 14 | 15 | class TestCodecs(unittest.TestCase): 16 | def setUp(self): 17 | self.loop = asyncio.new_event_loop() 18 | 19 | def test_bytes_to_hex_str(self): 20 | ret = bytes_to_hex_str(b'\x7f') 21 | self.assertEqual(ret, '0x7f') 22 | 23 | def test_bytes_to_int(self): 24 | ret = bytes_to_int(b'\x7f') 25 | self.assertEqual(ret, 127) 26 | ret = bytes_to_int(b'\xff\xff') 27 | self.assertEqual(ret, 65535) 28 | 29 | def test_decode_string(self): 30 | stream = asyncio.StreamReader(loop=self.loop) 31 | stream.feed_data(b'\x00\x02AA') 32 | ret = self.loop.run_until_complete(decode_string(stream)) 33 | self.assertEqual(ret, 'AA') 34 | 35 | def test_encode_string(self): 36 | encoded = encode_string('AA') 37 | self.assertEqual(b'\x00\x02AA', encoded) 38 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [flake8] 2 | ignore = 3 | E265, # block comment should start with '# ' (~64 warnings) 4 | E501, # line too long (~664 warnings) 5 | 6 | [tox] 7 | envlist = 8 | py34, 9 | py35, 10 | py36, 11 | py37, 12 | coverage, 13 | flake8, 14 | check-manifest 15 | 16 | [testenv] 17 | deps = nose 18 | commands = nosetests 19 | 20 | [testenv:flake8] 21 | deps = flake8 22 | commands = flake8 23 | 24 | [testenv:coverage] 25 | commands = 26 | python -m coverage erase 27 | python -m coverage run --branch --source=hbmqtt -m unittest 28 | python -m coverage report 29 | deps = coverage 30 | 31 | [testenv:check-manifest] 32 | deps = check-manifest 33 | commands = check-manifest 34 | --------------------------------------------------------------------------------