././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1579961563.696805 hbmqtt-0.9.6/0000755000175000017510000000000000000000000012731 5ustar00niconico00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1579961068.0 hbmqtt-0.9.6/MANIFEST.in0000644000175000017510000000064400000000000014473 0ustar00niconico00000000000000include *.rst include *.txt include license.txt include samples/passwd include tests/plugins/passwd include tox.ini recursive-include docs *.css recursive-include docs *.html recursive-include docs *.py recursive-include docs *.rst recursive-include docs Makefile recursive-include samples *.crt recursive-include samples *.py recursive-include scripts *.yaml recursive-include tests *.crt recursive-include tests *.py ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1579961563.696805 hbmqtt-0.9.6/PKG-INFO0000644000175000017510000000135700000000000014034 0ustar00niconico00000000000000Metadata-Version: 1.1 Name: hbmqtt Version: 0.9.6 Summary: MQTT client/broker using Python 3.4 asyncio library Home-page: https://github.com/beerfactory/hbmqtt Author: Nicolas Jouanin Author-email: nico@beerfactory.org License: MIT Description: UNKNOWN Platform: all Classifier: Development Status :: 3 - Alpha Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: MIT License Classifier: Operating System :: POSIX Classifier: Operating System :: MacOS Classifier: Operating System :: Microsoft :: Windows Classifier: Programming Language :: Python :: 3.4 Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: 3.6 Classifier: Topic :: Communications Classifier: Topic :: Internet ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1579961563.6868052 hbmqtt-0.9.6/docs/0000755000175000017510000000000000000000000013661 5ustar00niconico00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1579961068.0 hbmqtt-0.9.6/docs/Makefile0000644000175000017510000001636100000000000015330 0ustar00niconico00000000000000# Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = BUILDDIR = _build # User-friendly check for sphinx-build ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) $(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/) endif # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . # the i18n builder cannot share the environment and doctrees with the others I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest coverage gettext help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " dirhtml to make HTML files named index.html in directories" @echo " singlehtml to make a single large HTML file" @echo " pickle to make pickle files" @echo " json to make JSON files" @echo " htmlhelp to make HTML files and a HTML help project" @echo " qthelp to make HTML files and a qthelp project" @echo " applehelp to make an Apple Help Book" @echo " devhelp to make HTML files and a Devhelp project" @echo " epub to make an epub" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " latexpdf to make LaTeX files and run them through pdflatex" @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" @echo " text to make text files" @echo " man to make manual pages" @echo " texinfo to make Texinfo files" @echo " info to make Texinfo files and run them through makeinfo" @echo " gettext to make PO message catalogs" @echo " changes to make an overview of all changed/added/deprecated items" @echo " xml to make Docutils-native XML files" @echo " pseudoxml to make pseudoxml-XML files for display purposes" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" @echo " coverage to run coverage check of the documentation (if enabled)" clean: rm -rf $(BUILDDIR)/* html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." singlehtml: $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml @echo @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." htmlhelp: $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." qthelp: $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/HBMQTT.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/HBMQTT.qhc" applehelp: $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp @echo @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." @echo "N.B. You won't be able to view it unless you put it in" \ "~/Library/Documentation/Help or install it in your application" \ "bundle." devhelp: $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp @echo @echo "Build finished." @echo "To view the help file:" @echo "# mkdir -p $$HOME/.local/share/devhelp/HBMQTT" @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/HBMQTT" @echo "# devhelp" epub: $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub @echo @echo "Build finished. The epub file is in $(BUILDDIR)/epub." latex: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make' in that directory to run these through (pdf)latex" \ "(use \`make latexpdf' here to do that automatically)." latexpdf: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through pdflatex..." $(MAKE) -C $(BUILDDIR)/latex all-pdf @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." latexpdfja: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through platex and dvipdfmx..." $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." text: $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text @echo @echo "Build finished. The text files are in $(BUILDDIR)/text." man: $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man @echo @echo "Build finished. The manual pages are in $(BUILDDIR)/man." texinfo: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." @echo "Run \`make' in that directory to run these through makeinfo" \ "(use \`make info' here to do that automatically)." info: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo "Running Texinfo files through makeinfo..." make -C $(BUILDDIR)/texinfo info @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." gettext: $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale @echo @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." coverage: $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage @echo "Testing of coverage in the sources finished, look at the " \ "results in $(BUILDDIR)/coverage/python.txt." xml: $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml @echo @echo "Build finished. The XML files are in $(BUILDDIR)/xml." pseudoxml: $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml @echo @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1579961563.6868052 hbmqtt-0.9.6/docs/_static/0000755000175000017510000000000000000000000015307 5ustar00niconico00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1579961068.0 hbmqtt-0.9.6/docs/_static/theme_overrides.css0000644000175000017510000000005100000000000021201 0ustar00niconico00000000000000.wy-nav-content { max-width: 1100px }././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1579961563.6868052 hbmqtt-0.9.6/docs/_templates/0000755000175000017510000000000000000000000016016 5ustar00niconico00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1579961068.0 hbmqtt-0.9.6/docs/_templates/layout.html0000644000175000017510000000130100000000000020214 0ustar00niconico00000000000000{% extends "!layout.html" %} {% block footer %} {{ super() }} {% endblock %} ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1579961068.0 hbmqtt-0.9.6/docs/changelog.rst0000644000175000017510000000323500000000000016345 0ustar00niconico00000000000000Changelog --------- 0.9.5 ..... * fix `more issues `_ * fix a `few issues `_ 0.9.2 ..... * fix a `few issues `_ 0.9.1 ..... * See commit log 0.9.0 ..... * fix a `serie of issues `_ * improve plugin performance * support Python 3.6 * upgrade to ``websockets`` 3.3.0 0.8.0 ..... * fix a `serie of issues `_ 0.7.3 ..... * fix deliver message client method to raise TimeoutError (`#40 `_) * fix topic filter matching in broker (`#41 `_) Version 0.7.2 has been jumped due to troubles with pypi... 0.7.1 ..... * Fix `duplicated $SYS topic name `_ . 0.7.0 ..... * Fix a `serie of issues `_ reported by `Christoph Krey `_ 0.6.3 ..... * Fix issue `#22 `_. 0.6.2 ..... * Fix issue `#20 `_ (``mqtt`` subprotocol was missing). * Upgrade to ``websockets`` 3.0. 0.6.1 ..... * Fix issue `#19 `_ 0.6 ... * Added compatibility with Python 3.5. * Rewritten documentation. * Add command-line tools :doc:`references/hbmqtt`, :doc:`references/hbmqtt_pub` and :doc:`references/hbmqtt_sub`.././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1579961068.0 hbmqtt-0.9.6/docs/conf.py0000644000175000017510000002412200000000000015161 0ustar00niconico00000000000000#!/usr/bin/env python3 # -*- coding: utf-8 -*- # # HBMQTT documentation build configuration file, created by # sphinx-quickstart on Sun Nov 1 14:30:37 2015. # # This file is execfile()d with the current directory set to its # containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. import os # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. #sys.path.insert(0, os.path.abspath('.')) # -- General configuration ------------------------------------------------ # If your documentation needs a minimal Sphinx version, state it here. #needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ 'sphinx.ext.autodoc', 'sphinx.ext.doctest', 'sphinx.ext.intersphinx', 'sphinx.ext.todo', 'sphinx.ext.coverage', 'sphinx.ext.ifconfig', 'sphinx.ext.viewcode', ] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # The suffix(es) of source filenames. # You can specify multiple suffix as a list of string: # source_suffix = ['.rst', '.md'] source_suffix = '.rst' # The encoding of source files. #source_encoding = 'utf-8-sig' # The master toctree document. master_doc = 'index' # General information about the project. project = 'HBMQTT' copyright = '2015, Nicolas Jouanin' author = 'Nicolas Jouanin' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. version = '0.6' # The full version, including alpha/beta/rc tags. release = '0.6' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. # # This is also used if you do content translation via gettext catalogs. # Usually you set "language" from the command line for these cases. language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: #today = '' # Else, today_fmt is used as the format for a strftime call. #today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns = ['_build'] # The reST default role (used for this markup: `text`) to use for all # documents. #default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. #add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). #add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. #show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' # A list of ignored prefixes for module index sorting. #modindex_common_prefix = [] # If true, keep warnings as "system message" paragraphs in the built documents. #keep_warnings = False # If true, `todo` and `todoList` produce output, else they produce nothing. todo_include_todos = False # -- Options for HTML output ---------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. html_theme = 'default' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. #html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. #html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". #html_title = None # A shorter title for the navigation bar. Default is the same as html_title. #html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. #html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. #html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['_static'] # Add any extra paths that contain custom files (such as robots.txt or # .htaccess) here, relative to this directory. These files are copied # directly to the root of the documentation. #html_extra_path = [] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. #html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. #html_use_smartypants = True # Custom sidebar templates, maps document names to template names. #html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. #html_additional_pages = {} # If false, no module index is generated. #html_domain_indices = True # If false, no index is generated. #html_use_index = True # If true, the index is split into individual pages for each letter. #html_split_index = False # If true, links to the reST sources are added to the pages. #html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. #html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. #html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. #html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). #html_file_suffix = None # Language to be used for generating the HTML full-text search index. # Sphinx supports the following languages: # 'da', 'de', 'en', 'es', 'fi', 'fr', 'h', 'it', 'ja' # 'nl', 'no', 'pt', 'ro', 'r', 'sv', 'tr' #html_search_language = 'en' # A dictionary with options for the search language support, empty by default. # Now only 'ja' uses this config value #html_search_options = {'type': 'default'} # The name of a javascript file (relative to the configuration directory) that # implements a search results scorer. If empty, the default will be used. #html_search_scorer = 'scorer.js' # Output file base name for HTML help builder. htmlhelp_basename = 'HBMQTTdoc' # -- Options for LaTeX output --------------------------------------------- latex_elements = { # The paper size ('letterpaper' or 'a4paper'). #'papersize': 'letterpaper', # The font size ('10pt', '11pt' or '12pt'). #'pointsize': '10pt', # Additional stuff for the LaTeX preamble. #'preamble': '', # Latex figure (float) alignment #'figure_align': 'htbp', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ (master_doc, 'HBMQTT.tex', 'HBMQTT Documentation', 'Nicolas Jouanin', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of # the title page. #latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. #latex_use_parts = False # If true, show page references after internal links. #latex_show_pagerefs = False # If true, show URL addresses after external links. #latex_show_urls = False # Documents to append as an appendix to all manuals. #latex_appendices = [] # If false, no module index is generated. #latex_domain_indices = True # -- Options for manual page output --------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ (master_doc, 'hbmqtt', 'HBMQTT Documentation', [author], 1) ] # If true, show URL addresses after external links. #man_show_urls = False # -- Options for Texinfo output ------------------------------------------- # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ (master_doc, 'HBMQTT', 'HBMQTT Documentation', author, 'HBMQTT', 'One line description of project.', 'Miscellaneous'), ] # Documents to append as an appendix to all manuals. #texinfo_appendices = [] # If false, no module index is generated. #texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. #texinfo_show_urls = 'footnote' # If true, do not generate a @detailmenu in the "Top" node's menu. #texinfo_no_detailmenu = False # -- ReadTheDoc requirements and local template generation--------------------- # on_rtd is whether we are on readthedocs.org, this line of code grabbed from docs.readthedocs.org on_rtd = os.environ.get('READTHEDOCS', None) == 'True' if not on_rtd: # only import and set the theme if we're building docs locally import sphinx_rtd_theme html_theme = 'sphinx_rtd_theme' html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] # Override default css to get a larger width for local build def setup(app): #app.add_javascript("custom.js") app.add_stylesheet('theme_overrides.css') else: # Override default css to get a larger width for ReadTheDoc build html_context = { 'css_files': [ 'https://media.readthedocs.org/css/sphinx_rtd_theme.css', 'https://media.readthedocs.org/css/readthedocs-doc-embed.css', '_static/theme_overrides.css', ], } ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1579961068.0 hbmqtt-0.9.6/docs/index.rst0000644000175000017510000000444700000000000015533 0ustar00niconico00000000000000HBMQTT ====== ``HBMQTT`` is an open source `MQTT`_ client and broker implementation. Built on top of :mod:`asyncio`, Python's standard asynchronous I/O framework, HBMQTT provides a straightforward API based on coroutines, making it easy to write highly concurrent applications. Features -------- HBMQTT implements the full set of `MQTT 3.1.1`_ protocol specifications and provides the following features: - Support QoS 0, QoS 1 and QoS 2 messages flow - Client auto-reconnection on network lost - Authentication through password file (more methods can be added through a plugin system) - Basic ``$SYS`` topics - TCP and websocket support - SSL support over TCP and websocket - Plugin system Requirements ------------ HBMQTT is built on Python :mod:`asyncio` library which was introduced in Python 3.4. Tests have shown that HBMQTT run best with Python 3.4.3. Python 3.5.0 is also fully supported and recommended. Make sure you use one of these version before installing HBMQTT. Installation ------------ It is not recommended to install third-party library in Python system packages directory. The preferred way for installing HBMQTT is to create a virtual environment and then install all the dependencies you need. Refer to `PEP 405`_ to learn more. Once you have a environment setup and ready, HBMQTT can be installed with the following command :: (venv) $ pip install hbmqtt ``pip`` will download and install HBMQTT and all its dependencies. User guide ---------- 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. 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. 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. News and updates are listed in the :doc:`changelog`. .. _MQTT: http://www.mqtt.org .. _MQTT 3.1.1: http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html .. _PEP 405: https://www.python.org/dev/peps/pep-0405/ .. toctree:: :maxdepth: 2 :hidden: quickstart changelog references/index license ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1579961068.0 hbmqtt-0.9.6/docs/license.rst0000644000175000017510000000006400000000000016035 0ustar00niconico00000000000000License ------- .. literalinclude:: ../license.txt ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1579961068.0 hbmqtt-0.9.6/docs/quickstart.rst0000644000175000017510000000646000000000000016613 0ustar00niconico00000000000000Quickstart ========== A quick way for getting started with ``HBMQTT`` is to use console scripts provided for : * publishing a message on some topic on an external MQTT broker. * subscribing some topics and getting published messages. * running a autonomous MQTT broker These scripts are installed automatically when installing ``HBMQTT`` with the following command :: (venv) $ pip install hbmqtt Publishing messages ------------------- ``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. Publishing ```some_data`` to as ``/test`` topic on is as simple as : :: $ hbmqtt_pub --url mqtt://test.mosquitto.org -t /test -m some_data [2015-11-06 22:21:55,108] :: INFO - hbmqtt_pub/5135-MacBook-Pro.local Connecting to broker [2015-11-06 22:21:55,333] :: INFO - hbmqtt_pub/5135-MacBook-Pro.local Publishing to '/test' [2015-11-06 22:21:55,336] :: INFO - hbmqtt_pub/5135-MacBook-Pro.local Disconnected from broker This will use insecure TCP connection to connect to test.mosquitto.org. ``hbmqtt_pub`` also allows websockets and secure connection: :: $ hbmqtt_pub --url ws://test.mosquitto.org:8080 -t /test -m some_data [2015-11-06 22:22:42,542] :: INFO - hbmqtt_pub/5157-MacBook-Pro.local Connecting to broker [2015-11-06 22:22:42,924] :: INFO - hbmqtt_pub/5157-MacBook-Pro.local Publishing to '/test' [2015-11-06 22:22:52,926] :: INFO - hbmqtt_pub/5157-MacBook-Pro.local Disconnected from broker ``hbmqtt_pub`` can read from file or stdin and use data read as message payload: :: $ some_command | hbmqtt_pub --url mqtt://localhost -t /test -l See :doc:`references/hbmqtt_pub` reference documentation for details about available options and settings. Subscribing a topic ------------------- ``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. Subscribing a ``/test/#`` topic pattern is done with : :: $ hbmqtt_sub --url mqtt://localhost -t /test/# 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. See :doc:`references/hbmqtt_sub` reference documentation for details about available options and settings. URL Scheme ---------- 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 : :: [mqtt|ws][s]://[username][:password]@host.domain[:port] Here are some examples of URL: :: mqtt://localhost mqtt://localhost:1884 mqtt://user:password@localhost ws://test.mosquitto.org wss://user:password@localhost .. _MQTT URL scheme: https://github.com/mqtt/mqtt.github.io/wiki/URI-Scheme Running a broker ---------------- ``hbmqtt`` is a command-line tool for running a MQTT broker: :: $ hbmqtt [2015-11-06 22:45:16,470] :: INFO - Listener 'default' bind to 0.0.0.0:1883 (max_connections=-1) See :doc:`references/hbmqtt` reference documentation for details about available options and settings. ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1579961563.6901383 hbmqtt-0.9.6/docs/references/0000755000175000017510000000000000000000000016002 5ustar00niconico00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1579961068.0 hbmqtt-0.9.6/docs/references/broker.rst0000644000175000017510000001234000000000000020020 0ustar00niconico00000000000000Broker API reference ==================== 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. Usage example ------------- The following example shows how to start a broker using the default configuration: .. code-block:: python import logging import asyncio import os from hbmqtt.broker import Broker @asyncio.coroutine def broker_coro(): broker = Broker() yield from broker.start() if __name__ == '__main__': formatter = "[%(asctime)s] :: %(levelname)s :: %(name)s :: %(message)s" logging.basicConfig(level=logging.INFO, format=formatter) asyncio.get_event_loop().run_until_complete(broker_coro()) asyncio.get_event_loop().run_forever() When executed, this script gets the default event loop and asks it to run the ``broker_coro`` until it completes. ``broker_coro`` creates :class:`~hbmqtt.broker.Broker` instance and then :meth:`~hbmqtt.broker.Broker.start` the broker for serving. Once completed, the loop is ran forever, making this script never stop ... Reference --------- Broker API .......... .. automodule:: hbmqtt.broker .. autoclass:: Broker .. automethod:: start .. automethod:: shutdown Broker configuration .................... 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]_. .. code-block:: python listeners: default: max-connections: 50000 type: tcp my-tcp-1: bind: 127.0.0.1:1883 my-tcp-2: bind: 1.2.3.4:1884 max-connections: 1000 my-tcp-ssl-1: bind: 127.0.0.1:8885 ssl: on cafile: /some/cafile capath: /some/folder capath: certificate data certfile: /some/certfile keyfile: /some/key my-ws-1: bind: 0.0.0.0:8080 type: ws timeout-disconnect-delay: 2 auth: plugins: ['auth.anonymous'] #List of plugins to activate for authentication among all registered plugins allow-anonymous: true / false password-file: /some/passwd_file topic-check: enabled: true / false # Set to False if topic filtering is not needed plugins: ['topic_acl'] #List of plugins to activate for topic filtering among all registered plugins acl: # username: [list of allowed topics] username1: ['repositories/+/master', 'calendar/#', 'data/memes'] # List of topics on which client1 can publish and subscribe username2: ... anonymous: [] # List of topics on which an anonymous client can publish and subscribe 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: * ``bind``: IP address and port binding. * ``max-connections``: Set maximum number of active connection for the listener. ``0`` means no limit. * ``type``: transport protocol type; can be ``tcp`` for classic TCP listener or ``ws`` for MQTT over websocket. * ``ssl`` enables (``on``) or disable secured connection over the transport protocol. * ``cafile``, ``cadata``, ``certfile`` and ``keyfile`` : mandatory parameters for SSL secured connections. The ``auth`` section setup authentication behaviour: * ``plugins``: defines the list of activated plugins. Note the plugins must be defined in the ``hbmqtt.broker.plugins`` `entry point `_. * ``allow-anonymous`` : used by the internal :class:`hbmqtt.plugins.authentication.AnonymousAuthPlugin` plugin. This parameter enables (``on``) or disable anonymous connection, ie. connection without username. * ``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. The ``topic-check`` section setup access control policies for publishing and subscribing to topics: * ``enabled``: set to true if you want to impose an access control policy. Otherwise, set it to false. * ``plugins``: defines the list of activated plugins. Note the plugins must be defined in the ``hbmqtt.broker.plugins`` `entry point `_. * additional parameters: depending on the plugin used for access control, additional parameters should be added. * In case of ``topic_acl`` plugin, the Access Control List (ACL) must be defined in the parameter ``acl``. * For each username, a list with the allowed topics must be defined. * If the client logs in anonymously, the ``anonymous`` entry within the ACL is used in order to grant/deny subscriptions. .. [1] See `PyYAML `_ for loading YAML files as Python dict. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1579961068.0 hbmqtt-0.9.6/docs/references/common.rst0000644000175000017510000000064000000000000020024 0ustar00niconico00000000000000Common API ========== This document describes ``HBMQTT`` common API both used by :doc:`mqttclient` and :doc:`broker`. Reference --------- ApplicationMessage .................. .. automodule:: hbmqtt.session .. autoclass:: ApplicationMessage :members: .. autoclass:: IncomingApplicationMessage :show-inheritance: .. autoclass:: OutgoingApplicationMessage :show-inheritance: ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1579961068.0 hbmqtt-0.9.6/docs/references/hbmqtt.rst0000644000175000017510000000611400000000000020035 0ustar00niconico00000000000000hbmqtt ====== ``hbmqtt`` is a command-line script for running a MQTT 3.1.1 broker. Usage ----- ``hbmqtt`` usage : :: hbmqtt --version hbmqtt (-h | --help) hbmqtt [-c ] [-d] Options ------- --version HBMQTT version information -h, --help Display ``hbmqtt_sub`` usage help -c Set the YAML configuration file to read and pass to the client runtime. Configuration ------------- 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 : * ``listeners`` : network bindings configuration list * ``timeout-disconnect-delay`` : client disconnect timeout after keep-alive timeout * ``auth`` : authentication configuration Without the ``-c`` argument, the broker will run with the following default configuration: .. code-block:: yaml listeners: default: type: tcp bind: 0.0.0.0:1883 sys_interval: 20 auth: allow-anonymous: true plugins: - auth_file - auth_anonymous Using this configuration, ``hbmqtt`` will start a broker : * listening on TCP port 1883 on all network interfaces. * Publishing ``$SYS``_ update messages every ``20`` seconds. * Allowing anonymous login, and password file bases authentication. .. _YAML: http://yaml.org/ Configuration example --------------------- .. code-block:: yaml listeners: default: max-connections: 50000 type: tcp my-tcp-1: bind: 127.0.0.1:1883 my-tcp-2: bind: 1.2.3.4:1883 max-connections: 1000 my-tcp-ssl-1: bind: 127.0.0.1:8883 ssl: on cafile: /some/cafile capath: /some/folder capath: certificate data certfile: /some/certfile keyfile: /some/key my-ws-1: bind: 0.0.0.0:8080 type: ws timeout-disconnect-delay: 2 auth: plugins: ['auth.anonymous'] allow-anonymous: true password-file: /some/passwd_file This configuration example shows use case of every parameter. The ``listeners`` section define 3 bindings : * ``my-tcp-1`` : a unsecured TCP listener on port 1883 allowing ``1000`` clients connections simultaneously * ``my-tcp-ssl-1`` : a secured TCP listener on port 8883 allowing ``50000`` clients connections simultaneously * ``my-ws-1`` : a unsecured websocket listener on port 8080 allowing ``50000`` clients connections simultaneously 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 : :: username:password where ``password`` should be the encrypted password. Use the ``mkpasswd -m sha-512`` command to build encoded passphrase. Password file example: :: # Test user with 'test' password encrypted with sha-512 test:$6$l4zQEHEcowc1Pnv4$HHrh8xnsZoLItQ8BmpFHM4r6q5UqK3DnXp2GaTm5zp5buQ7NheY3Xt9f6godVKbEtA.hOC7IEDwnok3pbAOip. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1579961068.0 hbmqtt-0.9.6/docs/references/hbmqtt_pub.rst0000644000175000017510000001255700000000000020713 0ustar00niconico00000000000000hbmqtt_pub ========== ``hbmqtt_pub`` is a MQTT client that publishes simple messages on a topic from the command line. Usage ----- ``hbmqtt_pub`` usage : :: hbmqtt_pub --version hbmqtt_pub (-h | --help) hbmqtt_pub --url BROKER_URL -t TOPIC (-f FILE | -l | -m MESSAGE | -n | -s) [-c CONFIG_FILE] [-i CLIENT_ID] [-d] [-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] Note that for simplicity, ``hbmqtt_pub`` uses mostly the same argument syntax as `mosquitto_pub`_. .. _mosquitto_pub: http://mosquitto.org/man/mosquitto_pub-1.html Options ------- --version HBMQTT version information -h, --help Display ``hbmqtt_pub`` usage help -c Set the YAML configuration file to read and pass to the client runtime. -d Enable debugging informations. --ca-file Define the path to a file containing PEM encoded CA certificates that are trusted. Used to enable SSL communication. --ca-path Define the path to a directory containing PEM encoded CA certificates that are trusted. Used to enable SSL communication. --ca-data Set the PEM encoded CA certificates that are trusted. Used to enable SSL communication. --clean-session If given, set the CONNECT clean session flag to True. -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. -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. -l Send messages read from stdin. ``hbmqtt_pub`` will publish a message for each line read. Blank lines won't be sent. -k Set the CONNECT keep alive timeout. -m Send a single message from the command line. -n Send a null (zero length) message. -q, --qos Specify the quality of service to use for the message, from 0, 1 and 2. Defaults to 0. -s Send a message read from stdin, sending the entire content as a single message. -t The MQTT topic on which to publish the message. --url Broker connection URL, conforming to `MQTT URL scheme`_. --will-topic The topic on which to send a Will, in the event that the client disconnects unexpectedly. --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``. --will-qos The QoS to use for the Will. Defaults to 0. This must be used in conjunction with ``--will-topic``. --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``. --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 .. _MQTT URL scheme: https://github.com/mqtt/mqtt.github.io/wiki/URI-Scheme Configuration ------------- 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 : * ``keep_alive`` : Keep-alive timeout sent to the broker. Defaults to ``10`` seconds. * ``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. * ``default_qos`` : Default QoS for messages published. Defaults to 0. * ``default_retain`` : Default retain value to messages published. Defaults to ``false``. * ``auto_reconnect`` : Enable or disable auto-reconnect if connectection with the broker is interrupted. Defaults to ``false``. * ``reconnect_retries`` : Maximum reconnection retries. Defaults to ``2``. Negative value will cause client to reconnect infinietly. * ``reconnect_max_interval`` : Maximum interval between 2 connection retry. Defaults to ``10``. .. _YAML: http://yaml.org/ Examples -------- Examples below are adapted from `mosquitto_pub`_ documentation. Publish temperature information to localhost with QoS 1: :: hbmqtt_pub --url mqtt://localhost -t sensors/temperature -m 32 -q 1 Publish timestamp and temperature information to a remote host on a non-standard port and QoS 0: :: hbmqtt_pub --url mqtt://192.168.1.1:1885 -t sensors/temperature -m "1266193804 32" Publish light switch status. Message is set to retained because there may be a long period of time between light switch events: :: hbmqtt_pub --url mqtt://localhost -r -t switches/kitchen_lights/status -m "on" Send the contents of a file in two ways: :: hbmqtt_pub --url mqtt://localhost -t my/topic -f ./data hbmqtt_pub --url mqtt://localhost -t my/topic -s < ./data Publish temperature information to localhost with QoS 1 over mqtt encapsulated in a websocket connection and additional headers: :: hbmqtt_pub --url wss://localhost -t sensors/temperature -m 32 -q 1 --extra-headers '{"Authorization": "Bearer "}' .. _mosquitto_pub : http://mosquitto.org/man/mosquitto_pub-1.html ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1579961068.0 hbmqtt-0.9.6/docs/references/hbmqtt_sub.rst0000644000175000017510000001060400000000000020705 0ustar00niconico00000000000000hbmqtt_sub ========== ``hbmqtt_sub`` is a command line MQTT client that subscribes to some topics and output data received from messages published. Usage ----- ``hbmqtt_sub`` usage : :: hbmqtt_sub --version hbmqtt_sub (-h | --help) hbmqtt_sub --url BROKER_URL -t TOPIC... [-n COUNT] [-c CONFIG_FILE] [-i CLIENT_ID] [-q | --qos QOS] [-d] [-k KEEP_ALIVE] [--clean-session] [--ca-file CAFILE] [--ca-path CAPATH] [--ca-data CADATA] [ --will-topic WILL_TOPIC [--will-message WILL_MESSAGE] [--will-qos WILL_QOS] [--will-retain] ] [--extra-headers HEADER] Note that for simplicity, ``hbmqtt_sub`` uses mostly the same argument syntax as `mosquitto_sub`_. Options ------- --version HBMQTT version information -h, --help Display ``hbmqtt_sub`` usage help -c Set the YAML configuration file to read and pass to the client runtime. -d Enable debugging informations. --ca-file Define the path to a file containing PEM encoded CA certificates that are trusted. Used to enable SSL communication. --ca-path Define the path to a directory containing PEM encoded CA certificates that are trusted. Used to enable SSL communication. --ca-data Set the PEM encoded CA certificates that are trusted. Used to enable SSL communication. --clean-session If given, set the CONNECT clean session flag to True. -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. -k Set the CONNECT keep alive timeout. -n Number of messages to read before ending. Read forever if not given. -q, --qos Specify the quality of service to use for receiving messages. This QoS is sent in the subscribe request. -t Topic filters to subcribe. --url Broker connection URL, conforming to `MQTT URL scheme`_. --will-topic The topic on which to send a Will, in the event that the client disconnects unexpectedly. --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``. --will-qos The QoS to use for the Will. Defaults to 0. This must be used in conjunction with ``--will-topic``. --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``. --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 .. _MQTT URL scheme: https://github.com/mqtt/mqtt.github.io/wiki/URI-Scheme Configuration ------------- 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 : * ``keep_alive`` : Keep-alive timeout sent to the broker. Defaults to ``10`` seconds. * ``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. * ``default_qos`` : Default QoS for messages published. Defaults to 0. * ``default_retain`` : Default retain value to messages published. Defaults to ``false``. * ``auto_reconnect`` : Enable or disable auto-reconnect if connectection with the broker is interrupted. Defaults to ``false``. * ``reconnect_retries`` : Maximum reconnection retries. Defaults to ``2``. Negative value will cause client to reconnect infinietly. * ``reconnect_max_interval`` : Maximum interval between 2 connection retry. Defaults to ``10``. .. _YAML: http://yaml.org/ Examples -------- Examples below are adapted from `mosquitto_sub`_ documentation. Subscribe with QoS 0 to all messages published under $SYS/: :: hbmqtt_sub --url mqtt://localhost -t '$SYS/#' -q 0 Subscribe to 10 messages with QoS 2 from /#: :: hbmqtt_sub --url mqtt://localhost -t /# -q 2 -n 10 .. _mosquitto_sub : http://mosquitto.org/man/mosquitto_sub-1.html Subscribe with QoS 0 to all messages published under $SYS/: over mqtt encapsulated in a websocket connection and additional headers: :: hbmqtt_sub --url wss://localhost -t '$SYS/#' -q 0 --extra-headers '{"Authorization": "Bearer "}' ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1579961068.0 hbmqtt-0.9.6/docs/references/index.rst0000644000175000017510000000112200000000000017637 0ustar00niconico00000000000000References ========== Reference documentation for HBMQTT console scripts and programming API. Console scripts --------------- * :doc:`hbmqtt_pub` : MQTT client for publishing messages to a broker * :doc:`hbmqtt_sub` : MQTT client for subscribing to a topics and retrieved published messages * :doc:`hbmqtt` : Autonomous MQTT broker Programming API --------------- * :doc:`mqttclient` : MQTT client API reference * :doc:`broker` : MQTT broker API reference * :doc:`common` : Common API TBD .. toctree:: :hidden: hbmqtt_pub hbmqtt_sub hbmqtt mqttclient broker common ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1579961068.0 hbmqtt-0.9.6/docs/references/mqttclient.rst0000644000175000017510000002453400000000000020730 0ustar00niconico00000000000000MQTTClient API ============== The :class:`~hbmqtt.client.MQTTClient` class implements the client part of MQTT protocol. It can be used to publish and/or subscribe MQTT message on a broker accessible on the network through TCP or websocket protocol, both secured or unsecured. Usage examples -------------- Subscriber .......... The example below shows how to write a simple MQTT client which subscribes a topic and prints every messages received from the broker : .. code-block:: python import logging import asyncio from hbmqtt.client import MQTTClient, ClientException from hbmqtt.mqtt.constants import QOS_1, QOS_2 logger = logging.getLogger(__name__) @asyncio.coroutine def uptime_coro(): C = MQTTClient() yield from C.connect('mqtt://test.mosquitto.org/') # Subscribe to '$SYS/broker/uptime' with QOS=1 # Subscribe to '$SYS/broker/load/#' with QOS=2 yield from C.subscribe([ ('$SYS/broker/uptime', QOS_1), ('$SYS/broker/load/#', QOS_2), ]) try: for i in range(1, 100): message = yield from C.deliver_message() packet = message.publish_packet print("%d: %s => %s" % (i, packet.variable_header.topic_name, str(packet.payload.data))) yield from C.unsubscribe(['$SYS/broker/uptime', '$SYS/broker/load/#']) yield from C.disconnect() except ClientException as ce: logger.error("Client exception: %s" % ce) if __name__ == '__main__': formatter = "[%(asctime)s] %(name)s {%(filename)s:%(lineno)d} %(levelname)s - %(message)s" logging.basicConfig(level=logging.DEBUG, format=formatter) asyncio.get_event_loop().run_until_complete(uptime_coro()) When executed, this script gets the default event loop and asks it to run the ``uptime_coro`` until it completes. ``uptime_coro`` starts by initializing a :class:`~hbmqtt.client.MQTTClient` instance. The coroutine then call :meth:`~hbmqtt.client.MQTTClient.connect` to connect to the broker, here ``test.mosquitto.org``. Once connected, the coroutine subscribes to some topics, and then wait for 100 messages. Each message received is simply written to output. Finally, the coroutine unsubscribes from topics and disconnects from the broker. Publisher ......... The example below uses the :class:`~hbmqtt.client.MQTTClient` class to implement a publisher. This test publish 3 messages asynchronously to the broker on a test topic. For the purposes of the test, each message is published with a different Quality Of Service. This example also shows to method for publishing message asynchronously. .. code-block:: python import logging import asyncio from hbmqtt.client import MQTTClient from hbmqtt.mqtt.constants import QOS_0, QOS_1, QOS_2 logger = logging.getLogger(__name__) @asyncio.coroutine def test_coro(): C = MQTTClient() yield from C.connect('mqtt://test.mosquitto.org/') tasks = [ asyncio.ensure_future(C.publish('a/b', b'TEST MESSAGE WITH QOS_0')), asyncio.ensure_future(C.publish('a/b', b'TEST MESSAGE WITH QOS_1', qos=QOS_1)), asyncio.ensure_future(C.publish('a/b', b'TEST MESSAGE WITH QOS_2', qos=QOS_2)), ] yield from asyncio.wait(tasks) logger.info("messages published") yield from C.disconnect() @asyncio.coroutine def test_coro2(): try: C = MQTTClient() ret = yield from C.connect('mqtt://test.mosquitto.org:1883/') message = yield from C.publish('a/b', b'TEST MESSAGE WITH QOS_0', qos=QOS_0) message = yield from C.publish('a/b', b'TEST MESSAGE WITH QOS_1', qos=QOS_1) message = yield from C.publish('a/b', b'TEST MESSAGE WITH QOS_2', qos=QOS_2) #print(message) logger.info("messages published") yield from C.disconnect() except ConnectException as ce: logger.error("Connection failed: %s" % ce) asyncio.get_event_loop().stop() if __name__ == '__main__': formatter = "[%(asctime)s] %(name)s {%(filename)s:%(lineno)d} %(levelname)s - %(message)s" logging.basicConfig(level=logging.DEBUG, format=formatter) asyncio.get_event_loop().run_until_complete(test_coro()) asyncio.get_event_loop().run_until_complete(test_coro2()) As usual, the script runs the publish code through the async loop. ``test_coro()`` and ``test_coro()`` are ran in sequence. Both do the same job. ``test_coro()`` publish 3 messages in sequence. ``test_coro2()`` publishes the same message asynchronously. The difference appears the looking at the sequence of MQTT messages sent. ``test_coro()`` achieves: :: hbmqtt/YDYY;NNRpYQSy3?o -out-> PublishPacket(ts=2015-11-11 21:54:48.843901, fixed=MQTTFixedHeader(length=28, flags=0x0), variable=PublishVariableHeader(topic=a/b, packet_id=None), payload=PublishPayload(data="b'TEST MESSAGE WITH QOS_0'")) hbmqtt/YDYY;NNRpYQSy3?o -out-> PublishPacket(ts=2015-11-11 21:54:48.844152, fixed=MQTTFixedHeader(length=30, flags=0x2), variable=PublishVariableHeader(topic=a/b, packet_id=1), payload=PublishPayload(data="b'TEST MESSAGE WITH QOS_1'")) hbmqtt/YDYY;NNRpYQSy3?o <-in-- PubackPacket(ts=2015-11-11 21:54:48.979665, fixed=MQTTFixedHeader(length=2, flags=0x0), variable=PacketIdVariableHeader(packet_id=1), payload=None) hbmqtt/YDYY;NNRpYQSy3?o -out-> PublishPacket(ts=2015-11-11 21:54:48.980886, fixed=MQTTFixedHeader(length=30, flags=0x4), variable=PublishVariableHeader(topic=a/b, packet_id=2), payload=PublishPayload(data="b'TEST MESSAGE WITH QOS_2'")) hbmqtt/YDYY;NNRpYQSy3?o <-in-- PubrecPacket(ts=2015-11-11 21:54:49.029691, fixed=MQTTFixedHeader(length=2, flags=0x0), variable=PacketIdVariableHeader(packet_id=2), payload=None) hbmqtt/YDYY;NNRpYQSy3?o -out-> PubrelPacket(ts=2015-11-11 21:54:49.030823, fixed=MQTTFixedHeader(length=2, flags=0x2), variable=PacketIdVariableHeader(packet_id=2), payload=None) hbmqtt/YDYY;NNRpYQSy3?o <-in-- PubcompPacket(ts=2015-11-11 21:54:49.092514, fixed=MQTTFixedHeader(length=2, flags=0x0), variable=PacketIdVariableHeader(packet_id=2), payload=None)fixed=MQTTFixedHeader(length=2, flags=0x0), variable=PacketIdVariableHeader(packet_id=2), payload=None) while ``test_coro2()`` runs: :: hbmqtt/LYRf52W[56SOjW04 -out-> PublishPacket(ts=2015-11-11 21:54:48.466123, fixed=MQTTFixedHeader(length=28, flags=0x0), variable=PublishVariableHeader(topic=a/b, packet_id=None), payload=PublishPayload(data="b'TEST MESSAGE WITH QOS_0'")) hbmqtt/LYRf52W[56SOjW04 -out-> PublishPacket(ts=2015-11-11 21:54:48.466432, fixed=MQTTFixedHeader(length=30, flags=0x2), variable=PublishVariableHeader(topic=a/b, packet_id=1), payload=PublishPayload(data="b'TEST MESSAGE WITH QOS_1'")) hbmqtt/LYRf52W[56SOjW04 -out-> PublishPacket(ts=2015-11-11 21:54:48.466695, fixed=MQTTFixedHeader(length=30, flags=0x4), variable=PublishVariableHeader(topic=a/b, packet_id=2), payload=PublishPayload(data="b'TEST MESSAGE WITH QOS_2'")) hbmqtt/LYRf52W[56SOjW04 <-in-- PubackPacket(ts=2015-11-11 21:54:48.613062, fixed=MQTTFixedHeader(length=2, flags=0x0), variable=PacketIdVariableHeader(packet_id=1), payload=None) hbmqtt/LYRf52W[56SOjW04 <-in-- PubrecPacket(ts=2015-11-11 21:54:48.661073, fixed=MQTTFixedHeader(length=2, flags=0x0), variable=PacketIdVariableHeader(packet_id=2), payload=None) hbmqtt/LYRf52W[56SOjW04 -out-> PubrelPacket(ts=2015-11-11 21:54:48.661925, fixed=MQTTFixedHeader(length=2, flags=0x2), variable=PacketIdVariableHeader(packet_id=2), payload=None) hbmqtt/LYRf52W[56SOjW04 <-in-- PubcompPacket(ts=2015-11-11 21:54:48.713107, fixed=MQTTFixedHeader(length=2, flags=0x0), variable=PacketIdVariableHeader(packet_id=2), payload=None) Both coroutines have the same results except that ``test_coro2()`` manages messages flow in parallel which may be more efficient. Reference --------- MQTTClient API .............. .. automodule:: hbmqtt.client .. autoclass:: MQTTClient .. automethod:: connect .. automethod:: disconnect .. automethod:: reconnect .. automethod:: ping .. automethod:: publish .. automethod:: subscribe .. automethod:: unsubscribe .. automethod:: deliver_message MQTTClient configuration ........................ The :class:`~hbmqtt.client.MQTTClient` ``__init__`` method accepts a ``config`` parameter which allow to setup some behaviour and defaults settings. This argument must be a Python dict object which may contain the following entries: * ``keep_alive``: keep alive (in seconds) to send when connecting to the broker (defaults to ``10`` seconds). :class:`~hbmqtt.client.MQTTClient` will *auto-ping* the broker if not message is sent within the keep-alive interval. This avoids disconnection from the broker. * ``ping_delay``: *auto-ping* delay before keep-alive times out (defaults to ``1`` seconds). * ``default_qos``: Default QoS (``0``) used by :meth:`~hbmqtt.client.MQTTClient.publish` if ``qos`` argument is not given. * ``default_retain``: Default retain (``False``) used by :meth:`~hbmqtt.client.MQTTClient.publish` if ``qos`` argument is not given., * ``auto_reconnect``: enable or disable auto-reconnect feature (defaults to ``True``). * ``reconnect_max_interval``: maximum interval (in seconds) to wait before two connection retries (defaults to ``10``). * ``reconnect_retries``: maximum number of connect retries (defaults to ``2``). Negative value will cause client to reconnect infinietly. Default QoS and default retain can also be overriden by adding a ``topics`` with may contain QoS and retain values for specific topics. See the following example: .. code-block:: python config = { 'keep_alive': 10, 'ping_delay': 1, 'default_qos': 0, 'default_retain': False, 'auto_reconnect': True, 'reconnect_max_interval': 5, 'reconnect_retries': 10, 'topics': { '/test': { 'qos': 1 }, '/some_topic': { 'qos': 2, 'retain': True } } } With this setting any message published will set with QOS_0 and retain flag unset except for : * messages sent to ``/test`` topic : they will be sent with QOS_1 * messages sent to ``/some_topic`` topic : they will be sent with QOS_2 and retain flag set In any case, the ``qos`` and ``retain`` argument values passed to method :meth:`~hbmqtt.client.MQTTClient.publish` will override these settings. ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1579961563.6901383 hbmqtt-0.9.6/hbmqtt/0000755000175000017510000000000000000000000014230 5ustar00niconico00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1579961276.0 hbmqtt-0.9.6/hbmqtt/__init__.py0000644000175000017510000000017300000000000016342 0ustar00niconico00000000000000# Copyright (c) 2015 Nicolas JOUANIN # # See the file license.txt for copying permission. VERSION = (0, 9, 6, 'final', 0) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1579961068.0 hbmqtt-0.9.6/hbmqtt/adapters.py0000644000175000017510000001342200000000000016407 0ustar00niconico00000000000000# Copyright (c) 2015 Nicolas JOUANIN # # See the file license.txt for copying permission. import asyncio import io from websockets.protocol import WebSocketCommonProtocol from websockets.exceptions import ConnectionClosed from asyncio import StreamReader, StreamWriter import logging class ReaderAdapter: """ Base class for all network protocol reader adapter. Reader adapters are used to adapt read operations on the network depending on the protocol used """ @asyncio.coroutine def read(self, n=-1) -> bytes: """ Read up to n bytes. If n is not provided, or set to -1, read until EOF and return all read bytes. If the EOF was received and the internal buffer is empty, return an empty bytes object. :return: packet read as bytes data """ def feed_eof(self): """ Acknowleddge EOF """ class WriterAdapter: """ Base class for all network protocol writer adapter. Writer adapters are used to adapt write operations on the network depending on the protocol used """ def write(self, data): """ write some data to the protocol layer """ @asyncio.coroutine def drain(self): """ Let the write buffer of the underlying transport a chance to be flushed. """ def get_peer_info(self): """ Return peer socket info (remote address and remote port as tuple """ @asyncio.coroutine def close(self): """ Close the protocol connection """ class WebSocketsReader(ReaderAdapter): """ WebSockets API reader adapter This adapter relies on WebSocketCommonProtocol to read from a WebSocket. """ def __init__(self, protocol: WebSocketCommonProtocol): self._protocol = protocol self._stream = io.BytesIO(b'') @asyncio.coroutine def read(self, n=-1) -> bytes: yield from self._feed_buffer(n) data = self._stream.read(n) return data @asyncio.coroutine def _feed_buffer(self, n=1): """ Feed the data buffer by reading a Websocket message. :param n: if given, feed buffer until it contains at least n bytes """ buffer = bytearray(self._stream.read()) while len(buffer) < n: try: message = yield from self._protocol.recv() except ConnectionClosed: message = None if message is None: break if not isinstance(message, bytes): raise TypeError("message must be bytes") buffer.extend(message) self._stream = io.BytesIO(buffer) class WebSocketsWriter(WriterAdapter): """ WebSockets API writer adapter This adapter relies on WebSocketCommonProtocol to read from a WebSocket. """ def __init__(self, protocol: WebSocketCommonProtocol): self._protocol = protocol self._stream = io.BytesIO(b'') def write(self, data): """ write some data to the protocol layer """ self._stream.write(data) @asyncio.coroutine def drain(self): """ Let the write buffer of the underlying transport a chance to be flushed. """ data = self._stream.getvalue() if len(data): yield from self._protocol.send(data) self._stream = io.BytesIO(b'') def get_peer_info(self): return self._protocol.remote_address @asyncio.coroutine def close(self): yield from self._protocol.close() class StreamReaderAdapter(ReaderAdapter): """ Asyncio Streams API protocol adapter This adapter relies on StreamReader to read from a TCP socket. Because API is very close, this class is trivial """ def __init__(self, reader: StreamReader): self._reader = reader @asyncio.coroutine def read(self, n=-1) -> bytes: if n == -1: data = yield from self._reader.read(n) else: data = yield from self._reader.readexactly(n) return data def feed_eof(self): return self._reader.feed_eof() class StreamWriterAdapter(WriterAdapter): """ Asyncio Streams API protocol adapter This adapter relies on StreamWriter to write to a TCP socket. Because API is very close, this class is trivial """ def __init__(self, writer: StreamWriter): self.logger = logging.getLogger(__name__) self._writer = writer def write(self, data): self._writer.write(data) @asyncio.coroutine def drain(self): yield from self._writer.drain() def get_peer_info(self): extra_info = self._writer.get_extra_info('peername') return extra_info[0], extra_info[1] @asyncio.coroutine def close(self): yield from self._writer.drain() if self._writer.can_write_eof(): self._writer.write_eof() self._writer.close() class BufferReader(ReaderAdapter): """ Byte Buffer reader adapter This adapter simply adapt reading a byte buffer. """ def __init__(self, buffer: bytes): self._stream = io.BytesIO(buffer) @asyncio.coroutine def read(self, n=-1) -> bytes: return self._stream.read(n) class BufferWriter(WriterAdapter): """ ByteBuffer writer adapter This adapter simply adapt writing to a byte buffer """ def __init__(self, buffer=b''): self._stream = io.BytesIO(buffer) def write(self, data): """ write some data to the protocol layer """ self._stream.write(data) @asyncio.coroutine def drain(self): pass def get_buffer(self): return self._stream.getvalue() def get_peer_info(self): return "BufferWriter", 0 @asyncio.coroutine def close(self): self._stream.close() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1579961068.0 hbmqtt-0.9.6/hbmqtt/broker.py0000644000175000017510000011157300000000000016076 0ustar00niconico00000000000000# Copyright (c) 2015 Nicolas JOUANIN # # See the file license.txt for copying permission. import logging import ssl import websockets import asyncio import sys import re from asyncio import CancelledError from collections import deque from functools import partial from transitions import Machine, MachineError from hbmqtt.session import Session from hbmqtt.mqtt.protocol.broker_handler import BrokerProtocolHandler from hbmqtt.errors import HBMQTTException, MQTTException from hbmqtt.utils import format_client_message, gen_client_id from hbmqtt.adapters import ( StreamReaderAdapter, StreamWriterAdapter, ReaderAdapter, WriterAdapter, WebSocketsReader, WebSocketsWriter) from .plugins.manager import PluginManager, BaseContext _defaults = { 'timeout-disconnect-delay': 2, 'auth': { 'allow-anonymous': True, 'password-file': None }, } EVENT_BROKER_PRE_START = 'broker_pre_start' EVENT_BROKER_POST_START = 'broker_post_start' EVENT_BROKER_PRE_SHUTDOWN = 'broker_pre_shutdown' EVENT_BROKER_POST_SHUTDOWN = 'broker_post_shutdown' EVENT_BROKER_CLIENT_CONNECTED = 'broker_client_connected' EVENT_BROKER_CLIENT_DISCONNECTED = 'broker_client_disconnected' EVENT_BROKER_CLIENT_SUBSCRIBED = 'broker_client_subscribed' EVENT_BROKER_CLIENT_UNSUBSCRIBED = 'broker_client_unsubscribed' EVENT_BROKER_MESSAGE_RECEIVED = 'broker_message_received' class BrokerException(BaseException): pass class RetainedApplicationMessage: __slots__ = ('source_session', 'topic', 'data', 'qos') def __init__(self, source_session, topic, data, qos=None): self.source_session = source_session self.topic = topic self.data = data self.qos = qos class Server: def __init__(self, listener_name, server_instance, max_connections=-1, loop=None): self.logger = logging.getLogger(__name__) self.instance = server_instance self.conn_count = 0 self.listener_name = listener_name if loop is not None: self._loop = loop else: self._loop = asyncio.get_event_loop() self.max_connections = max_connections if self.max_connections > 0: self.semaphore = asyncio.Semaphore(self.max_connections, loop=self._loop) else: self.semaphore = None @asyncio.coroutine def acquire_connection(self): if self.semaphore: yield from self.semaphore.acquire() self.conn_count += 1 if self.max_connections > 0: self.logger.info("Listener '%s': %d/%d connections acquired" % (self.listener_name, self.conn_count, self.max_connections)) else: self.logger.info("Listener '%s': %d connections acquired" % (self.listener_name, self.conn_count)) def release_connection(self): if self.semaphore: self.semaphore.release() self.conn_count -= 1 if self.max_connections > 0: self.logger.info("Listener '%s': %d/%d connections acquired" % (self.listener_name, self.conn_count, self.max_connections)) else: self.logger.info("Listener '%s': %d connections acquired" % (self.listener_name, self.conn_count)) @asyncio.coroutine def close_instance(self): if self.instance: self.instance.close() yield from self.instance.wait_closed() class BrokerContext(BaseContext): """ BrokerContext is used as the context passed to plugins interacting with the broker. It act as an adapter to broker services from plugins developed for HBMQTT broker """ def __init__(self, broker): super().__init__() self.config = None self._broker_instance = broker @asyncio.coroutine def broadcast_message(self, topic, data, qos=None): yield from self._broker_instance.internal_message_broadcast(topic, data, qos) def retain_message(self, topic_name, data, qos=None): self._broker_instance.retain_message(None, topic_name, data, qos) @property def sessions(self): for k, session in self._broker_instance._sessions.items(): yield session[0] @property def retained_messages(self): return self._broker_instance._retained_messages @property def subscriptions(self): return self._broker_instance._subscriptions class Broker: """ MQTT 3.1.1 compliant broker implementation :param config: Example Yaml config :param loop: asyncio loop to use. Defaults to ``asyncio.get_event_loop()`` if none is given :param plugin_namespace: Plugin namespace to use when loading plugin entry_points. Defaults to ``hbmqtt.broker.plugins`` """ states = ['new', 'starting', 'started', 'not_started', 'stopping', 'stopped', 'not_stopped', 'stopped'] def __init__(self, config=None, loop=None, plugin_namespace=None): self.logger = logging.getLogger(__name__) self.config = _defaults if config is not None: self.config.update(config) self._build_listeners_config(self.config) if loop is not None: self._loop = loop else: self._loop = asyncio.get_event_loop() self._servers = dict() self._init_states() self._sessions = dict() self._subscriptions = dict() self._retained_messages = dict() self._broadcast_queue = asyncio.Queue(loop=self._loop) self._broadcast_task = None # Init plugins manager context = BrokerContext(self) context.config = self.config if plugin_namespace: namespace = plugin_namespace else: namespace = 'hbmqtt.broker.plugins' self.plugins_manager = PluginManager(namespace, context, self._loop) def _build_listeners_config(self, broker_config): self.listeners_config = dict() try: listeners_config = broker_config['listeners'] defaults = listeners_config['default'] for listener in listeners_config: config = dict(defaults) config.update(listeners_config[listener]) self.listeners_config[listener] = config except KeyError as ke: raise BrokerException("Listener config not found invalid: %s" % ke) def _init_states(self): self.transitions = Machine(states=Broker.states, initial='new') self.transitions.add_transition(trigger='start', source='new', dest='starting') self.transitions.add_transition(trigger='starting_fail', source='starting', dest='not_started') self.transitions.add_transition(trigger='starting_success', source='starting', dest='started') self.transitions.add_transition(trigger='shutdown', source='started', dest='stopping') self.transitions.add_transition(trigger='stopping_success', source='stopping', dest='stopped') self.transitions.add_transition(trigger='stopping_failure', source='stopping', dest='not_stopped') self.transitions.add_transition(trigger='start', source='stopped', dest='starting') @asyncio.coroutine def start(self): """ Start the broker to serve with the given configuration Start method opens network sockets and will start listening for incoming connections. This method is a *coroutine*. """ try: self._sessions = dict() self._subscriptions = dict() self._retained_messages = dict() self.transitions.start() self.logger.debug("Broker starting") except (MachineError, ValueError) as exc: # Backwards compat: MachineError is raised by transitions < 0.5.0. self.logger.warning("[WARN-0001] Invalid method call at this moment: %s" % exc) raise BrokerException("Broker instance can't be started: %s" % exc) yield from self.plugins_manager.fire_event(EVENT_BROKER_PRE_START) try: # Start network listeners for listener_name in self.listeners_config: listener = self.listeners_config[listener_name] if 'bind' not in listener: self.logger.debug("Listener configuration '%s' is not bound" % listener_name) else: # Max connections try: max_connections = listener['max_connections'] except KeyError: max_connections = -1 # SSL Context sc = None # accept string "on" / "off" or boolean ssl_active = listener.get('ssl', False) if isinstance(ssl_active, str): ssl_active = ssl_active.upper() == 'ON' if ssl_active: try: sc = ssl.create_default_context( ssl.Purpose.CLIENT_AUTH, cafile=listener.get('cafile'), capath=listener.get('capath'), cadata=listener.get('cadata') ) sc.load_cert_chain(listener['certfile'], listener['keyfile']) sc.verify_mode = ssl.CERT_OPTIONAL except KeyError as ke: raise BrokerException("'certfile' or 'keyfile' configuration parameter missing: %s" % ke) except FileNotFoundError as fnfe: raise BrokerException("Can't read cert files '%s' or '%s' : %s" % (listener['certfile'], listener['keyfile'], fnfe)) address, s_port = listener['bind'].split(':') port = 0 try: port = int(s_port) except ValueError as ve: raise BrokerException("Invalid port value in bind value: %s" % listener['bind']) if listener['type'] == 'tcp': cb_partial = partial(self.stream_connected, listener_name=listener_name) instance = yield from asyncio.start_server(cb_partial, address, port, reuse_address=True, ssl=sc, loop=self._loop) self._servers[listener_name] = Server(listener_name, instance, max_connections, self._loop) elif listener['type'] == 'ws': cb_partial = partial(self.ws_connected, listener_name=listener_name) instance = yield from websockets.serve(cb_partial, address, port, ssl=sc, loop=self._loop, subprotocols=['mqtt']) self._servers[listener_name] = Server(listener_name, instance, max_connections, self._loop) self.logger.info("Listener '%s' bind to %s (max_connections=%d)" % (listener_name, listener['bind'], max_connections)) self.transitions.starting_success() yield from self.plugins_manager.fire_event(EVENT_BROKER_POST_START) #Start broadcast loop self._broadcast_task = asyncio.ensure_future(self._broadcast_loop(), loop=self._loop) self.logger.debug("Broker started") except Exception as e: self.logger.error("Broker startup failed: %s" % e) self.transitions.starting_fail() raise BrokerException("Broker instance can't be started: %s" % e) @asyncio.coroutine def shutdown(self): """ Stop broker instance. Closes all connected session, stop listening on network socket and free resources. """ try: self._sessions = dict() self._subscriptions = dict() self._retained_messages = dict() self.transitions.shutdown() except (MachineError, ValueError) as exc: # Backwards compat: MachineError is raised by transitions < 0.5.0. self.logger.debug("Invalid method call at this moment: %s" % exc) raise BrokerException("Broker instance can't be stopped: %s" % exc) # Fire broker_shutdown event to plugins yield from self.plugins_manager.fire_event(EVENT_BROKER_PRE_SHUTDOWN) # Stop broadcast loop if self._broadcast_task: self._broadcast_task.cancel() if self._broadcast_queue.qsize() > 0: self.logger.warning("%d messages not broadcasted" % self._broadcast_queue.qsize()) for listener_name in self._servers: server = self._servers[listener_name] yield from server.close_instance() self.logger.debug("Broker closing") self.logger.info("Broker closed") yield from self.plugins_manager.fire_event(EVENT_BROKER_POST_SHUTDOWN) self.transitions.stopping_success() @asyncio.coroutine def internal_message_broadcast(self, topic, data, qos=None): return (yield from self._broadcast_message(None, topic, data)) @asyncio.coroutine def ws_connected(self, websocket, uri, listener_name): yield from self.client_connected(listener_name, WebSocketsReader(websocket), WebSocketsWriter(websocket)) @asyncio.coroutine def stream_connected(self, reader, writer, listener_name): yield from self.client_connected(listener_name, StreamReaderAdapter(reader), StreamWriterAdapter(writer)) @asyncio.coroutine def client_connected(self, listener_name, reader: ReaderAdapter, writer: WriterAdapter): # Wait for connection available on listener server = self._servers.get(listener_name, None) if not server: raise BrokerException("Invalid listener name '%s'" % listener_name) yield from server.acquire_connection() remote_address, remote_port = writer.get_peer_info() self.logger.info("Connection from %s:%d on listener '%s'" % (remote_address, remote_port, listener_name)) # Wait for first packet and expect a CONNECT try: handler, client_session = yield from BrokerProtocolHandler.init_from_connect(reader, writer, self.plugins_manager, loop=self._loop) except HBMQTTException as exc: self.logger.warning("[MQTT-3.1.0-1] %s: Can't read first packet an CONNECT: %s" % (format_client_message(address=remote_address, port=remote_port), exc)) #yield from writer.close() self.logger.debug("Connection closed") return except MQTTException as me: self.logger.error('Invalid connection from %s : %s' % (format_client_message(address=remote_address, port=remote_port), me)) yield from writer.close() self.logger.debug("Connection closed") return if client_session.clean_session: # Delete existing session and create a new one if client_session.client_id is not None and client_session.client_id != "": self.delete_session(client_session.client_id) else: client_session.client_id = gen_client_id() client_session.parent = 0 else: # Get session from cache if client_session.client_id in self._sessions: self.logger.debug("Found old session %s" % repr(self._sessions[client_session.client_id])) (client_session, h) = self._sessions[client_session.client_id] client_session.parent = 1 else: client_session.parent = 0 if client_session.keep_alive > 0: client_session.keep_alive += self.config['timeout-disconnect-delay'] self.logger.debug("Keep-alive timeout=%d" % client_session.keep_alive) handler.attach(client_session, reader, writer) self._sessions[client_session.client_id] = (client_session, handler) authenticated = yield from self.authenticate(client_session, self.listeners_config[listener_name]) if not authenticated: yield from writer.close() server.release_connection() # Delete client from connections list return while True: try: client_session.transitions.connect() break except (MachineError, ValueError): # Backwards compat: MachineError is raised by transitions < 0.5.0. self.logger.warning("Client %s is reconnecting too quickly, make it wait" % client_session.client_id) # Wait a bit may be client is reconnecting too fast yield from asyncio.sleep(1, loop=self._loop) yield from handler.mqtt_connack_authorize(authenticated) yield from self.plugins_manager.fire_event(EVENT_BROKER_CLIENT_CONNECTED, client_id=client_session.client_id) self.logger.debug("%s Start messages handling" % client_session.client_id) yield from handler.start() self.logger.debug("Retained messages queue size: %d" % client_session.retained_messages.qsize()) yield from self.publish_session_retained_messages(client_session) # Init and start loop for handling client messages (publish, subscribe/unsubscribe, disconnect) disconnect_waiter = asyncio.ensure_future(handler.wait_disconnect(), loop=self._loop) subscribe_waiter = asyncio.ensure_future(handler.get_next_pending_subscription(), loop=self._loop) unsubscribe_waiter = asyncio.ensure_future(handler.get_next_pending_unsubscription(), loop=self._loop) wait_deliver = asyncio.ensure_future(handler.mqtt_deliver_next_message(), loop=self._loop) connected = True while connected: try: done, pending = yield from asyncio.wait( [disconnect_waiter, subscribe_waiter, unsubscribe_waiter, wait_deliver], return_when=asyncio.FIRST_COMPLETED, loop=self._loop) if disconnect_waiter in done: result = disconnect_waiter.result() self.logger.debug("%s Result from wait_diconnect: %s" % (client_session.client_id, result)) if result is None: self.logger.debug("Will flag: %s" % client_session.will_flag) # Connection closed anormally, send will message if client_session.will_flag: self.logger.debug("Client %s disconnected abnormally, sending will message" % format_client_message(client_session)) yield from self._broadcast_message( client_session, client_session.will_topic, client_session.will_message, client_session.will_qos) if client_session.will_retain: self.retain_message(client_session, client_session.will_topic, client_session.will_message, client_session.will_qos) self.logger.debug("%s Disconnecting session" % client_session.client_id) yield from self._stop_handler(handler) client_session.transitions.disconnect() yield from self.plugins_manager.fire_event(EVENT_BROKER_CLIENT_DISCONNECTED, client_id=client_session.client_id) connected = False if unsubscribe_waiter in done: self.logger.debug("%s handling unsubscription" % client_session.client_id) unsubscription = unsubscribe_waiter.result() for topic in unsubscription['topics']: self._del_subscription(topic, client_session) yield from self.plugins_manager.fire_event( EVENT_BROKER_CLIENT_UNSUBSCRIBED, client_id=client_session.client_id, topic=topic) yield from handler.mqtt_acknowledge_unsubscription(unsubscription['packet_id']) unsubscribe_waiter = asyncio.Task(handler.get_next_pending_unsubscription(), loop=self._loop) if subscribe_waiter in done: self.logger.debug("%s handling subscription" % client_session.client_id) subscriptions = subscribe_waiter.result() return_codes = [] for subscription in subscriptions['topics']: result = yield from self.add_subscription(subscription, client_session) return_codes.append(result) yield from handler.mqtt_acknowledge_subscription(subscriptions['packet_id'], return_codes) for index, subscription in enumerate(subscriptions['topics']): if return_codes[index] != 0x80: yield from self.plugins_manager.fire_event( EVENT_BROKER_CLIENT_SUBSCRIBED, client_id=client_session.client_id, topic=subscription[0], qos=subscription[1]) yield from self.publish_retained_messages_for_subscription(subscription, client_session) subscribe_waiter = asyncio.Task(handler.get_next_pending_subscription(), loop=self._loop) self.logger.debug(repr(self._subscriptions)) if wait_deliver in done: if self.logger.isEnabledFor(logging.DEBUG): self.logger.debug("%s handling message delivery" % client_session.client_id) app_message = wait_deliver.result() if not app_message.topic: self.logger.warning("[MQTT-4.7.3-1] - %s invalid TOPIC sent in PUBLISH message, closing connection" % client_session.client_id) break if "#" in app_message.topic or "+" in app_message.topic: self.logger.warning("[MQTT-3.3.2-2] - %s invalid TOPIC sent in PUBLISH message, closing connection" % client_session.client_id) break yield from self.plugins_manager.fire_event(EVENT_BROKER_MESSAGE_RECEIVED, client_id=client_session.client_id, message=app_message) yield from self._broadcast_message(client_session, app_message.topic, app_message.data) if app_message.publish_packet.retain_flag: self.retain_message(client_session, app_message.topic, app_message.data, app_message.qos) wait_deliver = asyncio.Task(handler.mqtt_deliver_next_message(), loop=self._loop) except asyncio.CancelledError: self.logger.debug("Client loop cancelled") break disconnect_waiter.cancel() subscribe_waiter.cancel() unsubscribe_waiter.cancel() wait_deliver.cancel() self.logger.debug("%s Client disconnected" % client_session.client_id) server.release_connection() def _init_handler(self, session, reader, writer): """ Create a BrokerProtocolHandler and attach to a session :return: """ handler = BrokerProtocolHandler(self.plugins_manager, self._loop) handler.attach(session, reader, writer) return handler @asyncio.coroutine def _stop_handler(self, handler): """ Stop a running handler and detach if from the session :param handler: :return: """ try: yield from handler.stop() except Exception as e: self.logger.error(e) @asyncio.coroutine def authenticate(self, session: Session, listener): """ This method call the authenticate method on registered plugins to test user authentication. User is considered authenticated if all plugins called returns True. Plugins authenticate() method are supposed to return : - True if user is authentication succeed - False if user authentication fails - None if authentication can't be achieved (then plugin result is then ignored) :param session: :param listener: :return: """ auth_plugins = None auth_config = self.config.get('auth', None) if auth_config: auth_plugins = auth_config.get('plugins', None) returns = yield from self.plugins_manager.map_plugin_coro( "authenticate", session=session, filter_plugins=auth_plugins) auth_result = True if returns: for plugin in returns: res = returns[plugin] if res is False: auth_result = False self.logger.debug("Authentication failed due to '%s' plugin result: %s" % (plugin.name, res)) else: self.logger.debug("'%s' plugin result: %s" % (plugin.name, res)) # If all plugins returned True, authentication is success return auth_result @asyncio.coroutine def topic_filtering(self, session: Session, topic): """ This method call the topic_filtering method on registered plugins to check that the subscription is allowed. User is considered allowed if all plugins called return True. Plugins topic_filtering() method are supposed to return : - True if MQTT client can be subscribed to the topic - False if MQTT client is not allowed to subscribe to the topic - None if topic filtering can't be achieved (then plugin result is then ignored) :param session: :param listener: :param topic: Topic in which the client wants to subscribe :return: """ topic_plugins = None topic_config = self.config.get('topic-check', None) if topic_config and topic_config.get('enabled', False): topic_plugins = topic_config.get('plugins', None) returns = yield from self.plugins_manager.map_plugin_coro( "topic_filtering", session=session, topic=topic, filter_plugins=topic_plugins) topic_result = True if returns: for plugin in returns: res = returns[plugin] if res is False: topic_result = False self.logger.debug("Topic filtering failed due to '%s' plugin result: %s" % (plugin.name, res)) else: self.logger.debug("'%s' plugin result: %s" % (plugin.name, res)) # If all plugins returned True, authentication is success return topic_result def retain_message(self, source_session, topic_name, data, qos=None): if data is not None and data != b'': # If retained flag set, store the message for further subscriptions self.logger.debug("Retaining message on topic %s" % topic_name) retained_message = RetainedApplicationMessage(source_session, topic_name, data, qos) self._retained_messages[topic_name] = retained_message else: # [MQTT-3.3.1-10] if topic_name in self._retained_messages: self.logger.debug("Clear retained messages for topic '%s'" % topic_name) del self._retained_messages[topic_name] @asyncio.coroutine def add_subscription(self, subscription, session): try: a_filter = subscription[0] if '#' in a_filter and not a_filter.endswith('#'): # [MQTT-4.7.1-2] Wildcard character '#' is only allowed as last character in filter return 0x80 if a_filter != "+": if '+' in a_filter: if "/+" not in a_filter and "+/" not in a_filter: # [MQTT-4.7.1-3] + wildcard character must occupy entire level return 0x80 # Check if the client is authorised to connect to the topic permitted = yield from self.topic_filtering(session, topic=a_filter) if not permitted: return 0x80 qos = subscription[1] if 'max-qos' in self.config and qos > self.config['max-qos']: qos = self.config['max-qos'] if a_filter not in self._subscriptions: self._subscriptions[a_filter] = [] already_subscribed = next( (s for (s, qos) in self._subscriptions[a_filter] if s.client_id == session.client_id), None) if not already_subscribed: self._subscriptions[a_filter].append((session, qos)) else: self.logger.debug("Client %s has already subscribed to %s" % (format_client_message(session=session), a_filter)) return qos except KeyError: return 0x80 def _del_subscription(self, a_filter, session): """ Delete a session subscription on a given topic :param a_filter: :param session: :return: """ deleted = 0 try: subscriptions = self._subscriptions[a_filter] for index, (sub_session, qos) in enumerate(subscriptions): if sub_session.client_id == session.client_id: self.logger.debug("Removing subscription on topic '%s' for client %s" % (a_filter, format_client_message(session=session))) subscriptions.pop(index) deleted += 1 break except KeyError: # Unsubscribe topic not found in current subscribed topics pass finally: return deleted def _del_all_subscriptions(self, session): """ Delete all topic subscriptions for a given session :param session: :return: """ filter_queue = deque() for topic in self._subscriptions: if self._del_subscription(topic, session): filter_queue.append(topic) for topic in filter_queue: if not self._subscriptions[topic]: del self._subscriptions[topic] def matches(self, topic, a_filter): if "#" not in a_filter and "+" not in a_filter: # if filter doesn't contain wildcard, return exact match return a_filter == topic else: # else use regex match_pattern = re.compile(a_filter.replace('#', '.*').replace('$', '\$').replace('+', '[/\$\s\w\d]+')) return match_pattern.match(topic) @asyncio.coroutine def _broadcast_loop(self): running_tasks = deque() try: while True: while running_tasks and running_tasks[0].done(): running_tasks.popleft() broadcast = yield from self._broadcast_queue.get() if self.logger.isEnabledFor(logging.DEBUG): self.logger.debug("broadcasting %r" % broadcast) for k_filter in self._subscriptions: if broadcast['topic'].startswith("$") and (k_filter.startswith("+") or k_filter.startswith("#")): self.logger.debug("[MQTT-4.7.2-1] - ignoring brodcasting $ topic to subscriptions starting with + or #") elif self.matches(broadcast['topic'], k_filter): subscriptions = self._subscriptions[k_filter] for (target_session, qos) in subscriptions: if 'qos' in broadcast: qos = broadcast['qos'] if target_session.transitions.state == 'connected': self.logger.debug("broadcasting application message from %s on topic '%s' to %s" % (format_client_message(session=broadcast['session']), broadcast['topic'], format_client_message(session=target_session))) handler = self._get_handler(target_session) task = asyncio.ensure_future( handler.mqtt_publish(broadcast['topic'], broadcast['data'], qos, retain=False), loop=self._loop) running_tasks.append(task) else: self.logger.debug("retaining application message from %s on topic '%s' to client '%s'" % (format_client_message(session=broadcast['session']), broadcast['topic'], format_client_message(session=target_session))) retained_message = RetainedApplicationMessage( broadcast['session'], broadcast['topic'], broadcast['data'], qos) yield from target_session.retained_messages.put(retained_message) except CancelledError: # Wait until current broadcasting tasks end if running_tasks: yield from asyncio.wait(running_tasks, loop=self._loop) @asyncio.coroutine def _broadcast_message(self, session, topic, data, force_qos=None): broadcast = { 'session': session, 'topic': topic, 'data': data } if force_qos: broadcast['qos'] = force_qos yield from self._broadcast_queue.put(broadcast) @asyncio.coroutine def publish_session_retained_messages(self, session): self.logger.debug("Publishing %d messages retained for session %s" % (session.retained_messages.qsize(), format_client_message(session=session)) ) publish_tasks = [] handler = self._get_handler(session) while not session.retained_messages.empty(): retained = yield from session.retained_messages.get() publish_tasks.append(asyncio.ensure_future( handler.mqtt_publish( retained.topic, retained.data, retained.qos, True), loop=self._loop)) if publish_tasks: yield from asyncio.wait(publish_tasks, loop=self._loop) @asyncio.coroutine def publish_retained_messages_for_subscription(self, subscription, session): self.logger.debug("Begin broadcasting messages retained due to subscription on '%s' from %s" % (subscription[0], format_client_message(session=session))) publish_tasks = [] handler = self._get_handler(session) for d_topic in self._retained_messages: self.logger.debug("matching : %s %s" % (d_topic, subscription[0])) if self.matches(d_topic, subscription[0]): self.logger.debug("%s and %s match" % (d_topic, subscription[0])) retained = self._retained_messages[d_topic] publish_tasks.append(asyncio.Task( handler.mqtt_publish( retained.topic, retained.data, subscription[1], True), loop=self._loop)) if publish_tasks: yield from asyncio.wait(publish_tasks, loop=self._loop) self.logger.debug("End broadcasting messages retained due to subscription on '%s' from %s" % (subscription[0], format_client_message(session=session))) def delete_session(self, client_id): """ Delete an existing session data, for example due to clean session set in CONNECT :param client_id: :return: """ try: session = self._sessions[client_id][0] except KeyError: session = None if session is None: self.logger.debug("Delete session : session %s doesn't exist" % client_id) return # Delete subscriptions self.logger.debug("deleting session %s subscriptions" % repr(session)) self._del_all_subscriptions(session) self.logger.debug("deleting existing session %s" % repr(self._sessions[client_id])) del self._sessions[client_id] def _get_handler(self, session): client_id = session.client_id if client_id: try: return self._sessions[client_id][1] except KeyError: pass return None ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1579961068.0 hbmqtt-0.9.6/hbmqtt/client.py0000644000175000017510000005407000000000000016066 0ustar00niconico00000000000000# Copyright (c) 2015 Nicolas JOUANIN # # See the file license.txt for copying permission. import asyncio import logging import ssl import copy from urllib.parse import urlparse, urlunparse from functools import wraps from hbmqtt.utils import not_in_dict_or_none from hbmqtt.session import Session from hbmqtt.mqtt.connack import CONNECTION_ACCEPTED from hbmqtt.mqtt.protocol.client_handler import ClientProtocolHandler from hbmqtt.adapters import StreamReaderAdapter, StreamWriterAdapter, WebSocketsReader, WebSocketsWriter from hbmqtt.plugins.manager import PluginManager, BaseContext from hbmqtt.mqtt.protocol.handler import ProtocolHandlerException from hbmqtt.mqtt.constants import QOS_0, QOS_1, QOS_2 import websockets from websockets.uri import InvalidURI from websockets.exceptions import InvalidHandshake from collections import deque _defaults = { 'keep_alive': 10, 'ping_delay': 1, 'default_qos': 0, 'default_retain': False, 'auto_reconnect': True, 'reconnect_max_interval': 10, 'reconnect_retries': 2, } class ClientException(Exception): pass class ConnectException(ClientException): pass class ClientContext(BaseContext): """ ClientContext is used as the context passed to plugins interacting with the client. It act as an adapter to client services from plugins """ def __init__(self): super().__init__() self.config = None base_logger = logging.getLogger(__name__) def mqtt_connected(func): """ MQTTClient coroutines decorator which will wait until connection before calling the decorated method. :param func: coroutine to be called once connected :return: coroutine result """ @asyncio.coroutine @wraps(func) def wrapper(self, *args, **kwargs): if not self._connected_state.is_set(): base_logger.warning("Client not connected, waiting for it") _, pending = yield from asyncio.wait([self._connected_state.wait(), self._no_more_connections.wait()], return_when=asyncio.FIRST_COMPLETED) for t in pending: t.cancel() if self._no_more_connections.is_set(): raise ClientException("Will not reconnect") return (yield from func(self, *args, **kwargs)) return wrapper class MQTTClient: """ MQTT client implementation. MQTTClient instances provides API for connecting to a broker and send/receive messages using the MQTT protocol. :param client_id: MQTT client ID to use when connecting to the broker. If none, it will generated randomly by :func:`hbmqtt.utils.gen_client_id` :param config: Client configuration :param loop: asynio loop to use :return: class instance """ def __init__(self, client_id=None, config=None, loop=None): self.logger = logging.getLogger(__name__) self.config = copy.deepcopy(_defaults) if config is not None: self.config.update(config) if client_id is not None: self.client_id = client_id else: from hbmqtt.utils import gen_client_id self.client_id = gen_client_id() self.logger.debug("Using generated client ID : %s" % self.client_id) if loop is not None: self._loop = loop else: self._loop = asyncio.get_event_loop() self.session = None self._handler = None self._disconnect_task = None self._connected_state = asyncio.Event(loop=self._loop) self._no_more_connections = asyncio.Event(loop=self._loop) self.extra_headers = {} # Init plugins manager context = ClientContext() context.config = self.config self.plugins_manager = PluginManager('hbmqtt.client.plugins', context) self.client_tasks = deque() @asyncio.coroutine def connect(self, uri=None, cleansession=None, cafile=None, capath=None, cadata=None, extra_headers={}): """ Connect to a remote broker. At first, a network connection is established with the server using the given protocol (``mqtt``, ``mqtts``, ``ws`` or ``wss``). Once the socket is connected, a `CONNECT `_ message is sent with the requested informations. This method is a *coroutine*. :param uri: Broker URI connection, conforming to `MQTT URI scheme `_. Uses ``uri`` config attribute by default. :param cleansession: MQTT CONNECT clean session flag :param cafile: server certificate authority file (optional, used for secured connection) :param capath: server certificate authority path (optional, used for secured connection) :param cadata: server certificate authority data (optional, used for secured connection) :param extra_headers: a dictionary with additional http headers that should be sent on the initial connection (optional, used only with websocket connections) :return: `CONNACK `_ return code :raise: :class:`hbmqtt.client.ConnectException` if connection fails """ self.session = self._initsession(uri, cleansession, cafile, capath, cadata) self.extra_headers = extra_headers; self.logger.debug("Connect to: %s" % uri) try: return (yield from self._do_connect()) except BaseException as be: self.logger.warning("Connection failed: %r" % be) auto_reconnect = self.config.get('auto_reconnect', False) if not auto_reconnect: raise else: return (yield from self.reconnect()) @asyncio.coroutine def disconnect(self): """ Disconnect from the connected broker. This method sends a `DISCONNECT `_ message and closes the network socket. This method is a *coroutine*. """ yield from self.cancel_tasks() if self.session.transitions.is_connected(): if not self._disconnect_task.done(): self._disconnect_task.cancel() yield from self._handler.mqtt_disconnect() self._connected_state.clear() yield from self._handler.stop() self.session.transitions.disconnect() else: self.logger.warning("Client session is not currently connected, ignoring call") @asyncio.coroutine def cancel_tasks(self): """ Before disconnection need to cancel all pending tasks :return: """ try: while True: task = self.client_tasks.pop() task.cancel() except IndexError as err: pass @asyncio.coroutine def reconnect(self, cleansession=None): """ Reconnect a previously connected broker. Reconnection tries to establish a network connection and send a `CONNECT `_ message. Retries interval and attempts can be controled with the ``reconnect_max_interval`` and ``reconnect_retries`` configuration parameters. This method is a *coroutine*. :param cleansession: clean session flag used in MQTT CONNECT messages sent for reconnections. :return: `CONNACK `_ return code :raise: :class:`hbmqtt.client.ConnectException` if re-connection fails after max retries. """ if self.session.transitions.is_connected(): self.logger.warning("Client already connected") return CONNECTION_ACCEPTED if cleansession: self.session.clean_session = cleansession self.logger.debug("Reconnecting with session parameters: %s" % self.session) reconnect_max_interval = self.config.get('reconnect_max_interval', 10) reconnect_retries = self.config.get('reconnect_retries', 5) nb_attempt = 1 yield from asyncio.sleep(1, loop=self._loop) while True: try: self.logger.debug("Reconnect attempt %d ..." % nb_attempt) return (yield from self._do_connect()) except BaseException as e: self.logger.warning("Reconnection attempt failed: %r" % e) if reconnect_retries >= 0 and nb_attempt > reconnect_retries: self.logger.error("Maximum number of connection attempts reached. Reconnection aborted") raise ConnectException("Too many connection attempts failed") exp = 2 ** nb_attempt delay = exp if exp < reconnect_max_interval else reconnect_max_interval self.logger.debug("Waiting %d second before next attempt" % delay) yield from asyncio.sleep(delay, loop=self._loop) nb_attempt += 1 @asyncio.coroutine def _do_connect(self): return_code = yield from self._connect_coro() self._disconnect_task = asyncio.ensure_future(self.handle_connection_close(), loop=self._loop) return return_code @mqtt_connected @asyncio.coroutine def ping(self): """ Ping the broker. Send a MQTT `PINGREQ `_ message for response. This method is a *coroutine*. """ if self.session.transitions.is_connected(): yield from self._handler.mqtt_ping() else: self.logger.warning("MQTT PING request incompatible with current session state '%s'" % self.session.transitions.state) @mqtt_connected @asyncio.coroutine def publish(self, topic, message, qos=None, retain=None, ack_timeout=None): """ Publish a message to the broker. Send a MQTT `PUBLISH `_ message and wait for acknowledgment depending on Quality Of Service This method is a *coroutine*. :param topic: topic name to which message data is published :param message: payload message (as bytes) to send. :param qos: requested publish quality of service : QOS_0, QOS_1 or QOS_2. Defaults to ``default_qos`` config parameter or QOS_0. :param retain: retain flag. Defaults to ``default_retain`` config parameter or False. """ def get_retain_and_qos(): if qos: assert qos in (QOS_0, QOS_1, QOS_2) _qos = qos else: _qos = self.config['default_qos'] try: _qos = self.config['topics'][topic]['qos'] except KeyError: pass if retain: _retain = retain else: _retain = self.config['default_retain'] try: _retain = self.config['topics'][topic]['retain'] except KeyError: pass return _qos, _retain (app_qos, app_retain) = get_retain_and_qos() return (yield from self._handler.mqtt_publish(topic, message, app_qos, app_retain, ack_timeout)) @mqtt_connected @asyncio.coroutine def subscribe(self, topics): """ Subscribe to some topics. Send a MQTT `SUBSCRIBE `_ message and wait for broker acknowledgment. This method is a *coroutine*. :param topics: array of topics pattern to subscribe with associated QoS. :return: `SUBACK `_ message return code. Example of ``topics`` argument expected structure: :: [ ('$SYS/broker/uptime', QOS_1), ('$SYS/broker/load/#', QOS_2), ] """ return (yield from self._handler.mqtt_subscribe(topics, self.session.next_packet_id)) @mqtt_connected @asyncio.coroutine def unsubscribe(self, topics): """ Unsubscribe from some topics. Send a MQTT `UNSUBSCRIBE `_ message and wait for broker `UNSUBACK `_ message. This method is a *coroutine*. :param topics: array of topics to unsubscribe from. Example of ``topics`` argument expected structure: :: ['$SYS/broker/uptime', '$SYS/broker/load/#'] """ yield from self._handler.mqtt_unsubscribe(topics, self.session.next_packet_id) @asyncio.coroutine def deliver_message(self, timeout=None): """ Deliver next received message. Deliver next message received from the broker. If no message is available, this methods waits until next message arrives or ``timeout`` occurs. This method is a *coroutine*. :param timeout: maximum number of seconds to wait before returning. If timeout is not specified or None, there is no limit to the wait time until next message arrives. :return: instance of :class:`hbmqtt.session.ApplicationMessage` containing received message information flow. :raises: :class:`asyncio.TimeoutError` if timeout occurs before a message is delivered """ deliver_task = asyncio.ensure_future(self._handler.mqtt_deliver_next_message(), loop=self._loop) self.client_tasks.append(deliver_task) self.logger.debug("Waiting message delivery") done, pending = yield from asyncio.wait([deliver_task], loop=self._loop, return_when=asyncio.FIRST_EXCEPTION, timeout=timeout) if deliver_task in done: if deliver_task.exception() is not None: # deliver_task raised an exception, pass it on to our caller raise deliver_task.exception() self.client_tasks.pop() return deliver_task.result() else: #timeout occured before message received deliver_task.cancel() raise asyncio.TimeoutError @asyncio.coroutine def _connect_coro(self): kwargs = dict() # Decode URI attributes uri_attributes = urlparse(self.session.broker_uri) scheme = uri_attributes.scheme secure = True if scheme in ('mqtts', 'wss') else False self.session.username = self.session.username if self.session.username else uri_attributes.username self.session.password = self.session.password if self.session.password else uri_attributes.password self.session.remote_address = uri_attributes.hostname self.session.remote_port = uri_attributes.port if scheme in ('mqtt', 'mqtts') and not self.session.remote_port: self.session.remote_port = 8883 if scheme == 'mqtts' else 1883 if scheme in ('ws', 'wss') and not self.session.remote_port: self.session.remote_port = 443 if scheme == 'wss' else 80 if scheme in ('ws', 'wss'): # Rewrite URI to conform to https://tools.ietf.org/html/rfc6455#section-3 uri = (scheme, self.session.remote_address + ":" + str(self.session.remote_port), uri_attributes[2], uri_attributes[3], uri_attributes[4], uri_attributes[5]) self.session.broker_uri = urlunparse(uri) # Init protocol handler #if not self._handler: self._handler = ClientProtocolHandler(self.plugins_manager, loop=self._loop) if secure: sc = ssl.create_default_context( ssl.Purpose.SERVER_AUTH, cafile=self.session.cafile, capath=self.session.capath, cadata=self.session.cadata) if 'certfile' in self.config and 'keyfile' in self.config: sc.load_cert_chain(self.config['certfile'], self.config['keyfile']) if 'check_hostname' in self.config and isinstance(self.config['check_hostname'], bool): sc.check_hostname = self.config['check_hostname'] kwargs['ssl'] = sc try: reader = None writer = None self._connected_state.clear() # Open connection if scheme in ('mqtt', 'mqtts'): conn_reader, conn_writer = \ yield from asyncio.open_connection( self.session.remote_address, self.session.remote_port, loop=self._loop, **kwargs) reader = StreamReaderAdapter(conn_reader) writer = StreamWriterAdapter(conn_writer) elif scheme in ('ws', 'wss'): websocket = yield from websockets.connect( self.session.broker_uri, subprotocols=['mqtt'], loop=self._loop, extra_headers=self.extra_headers, **kwargs) reader = WebSocketsReader(websocket) writer = WebSocketsWriter(websocket) # Start MQTT protocol self._handler.attach(self.session, reader, writer) return_code = yield from self._handler.mqtt_connect() if return_code is not CONNECTION_ACCEPTED: self.session.transitions.disconnect() self.logger.warning("Connection rejected with code '%s'" % return_code) exc = ConnectException("Connection rejected by broker") exc.return_code = return_code raise exc else: # Handle MQTT protocol yield from self._handler.start() self.session.transitions.connect() self._connected_state.set() self.logger.debug("connected to %s:%s" % (self.session.remote_address, self.session.remote_port)) return return_code except InvalidURI as iuri: self.logger.warning("connection failed: invalid URI '%s'" % self.session.broker_uri) self.session.transitions.disconnect() raise ConnectException("connection failed: invalid URI '%s'" % self.session.broker_uri, iuri) except InvalidHandshake as ihs: self.logger.warning("connection failed: invalid websocket handshake") self.session.transitions.disconnect() raise ConnectException("connection failed: invalid websocket handshake", ihs) except (ProtocolHandlerException, ConnectionError, OSError) as e: self.logger.warning("MQTT connection failed: %r" % e) self.session.transitions.disconnect() raise ConnectException(e) @asyncio.coroutine def handle_connection_close(self): def cancel_tasks(): self._no_more_connections.set() while self.client_tasks: task = self.client_tasks.popleft() if not task.done(): task.set_exception(ClientException("Connection lost")) self.logger.debug("Watch broker disconnection") # Wait for disconnection from broker (like connection lost) yield from self._handler.wait_disconnect() self.logger.warning("Disconnected from broker") # Block client API self._connected_state.clear() # stop an clean handler #yield from self._handler.stop() self._handler.detach() self.session.transitions.disconnect() if self.config.get('auto_reconnect', False): # Try reconnection self.logger.debug("Auto-reconnecting") try: yield from self.reconnect() except ConnectException: # Cancel client pending tasks cancel_tasks() else: # Cancel client pending tasks cancel_tasks() def _initsession( self, uri=None, cleansession=None, cafile=None, capath=None, cadata=None) -> Session: # Load config broker_conf = self.config.get('broker', dict()).copy() if uri: broker_conf['uri'] = uri if cafile: broker_conf['cafile'] = cafile elif 'cafile' not in broker_conf: broker_conf['cafile'] = None if capath: broker_conf['capath'] = capath elif 'capath' not in broker_conf: broker_conf['capath'] = None if cadata: broker_conf['cadata'] = cadata elif 'cadata' not in broker_conf: broker_conf['cadata'] = None if cleansession is not None: broker_conf['cleansession'] = cleansession for key in ['uri']: if not_in_dict_or_none(broker_conf, key): raise ClientException("Missing connection parameter '%s'" % key) s = Session() s.broker_uri = uri s.client_id = self.client_id s.cafile = broker_conf['cafile'] s.capath = broker_conf['capath'] s.cadata = broker_conf['cadata'] if cleansession is not None: s.clean_session = cleansession else: s.clean_session = self.config.get('cleansession', True) s.keep_alive = self.config['keep_alive'] - self.config['ping_delay'] if 'will' in self.config: s.will_flag = True s.will_retain = self.config['will']['retain'] s.will_topic = self.config['will']['topic'] s.will_message = self.config['will']['message'] s.will_qos = self.config['will']['qos'] else: s.will_flag = False s.will_retain = False s.will_topic = None s.will_message = None return s ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1579961068.0 hbmqtt-0.9.6/hbmqtt/codecs.py0000644000175000017510000000642500000000000016051 0ustar00niconico00000000000000# Copyright (c) 2015 Nicolas JOUANIN # # See the file license.txt for copying permission. import asyncio from struct import pack, unpack from hbmqtt.errors import NoDataException def bytes_to_hex_str(data): """ converts a sequence of bytes into its displayable hex representation, ie: 0x?????? :param data: byte sequence :return: Hexadecimal displayable representation """ return '0x' + ''.join(format(b, '02x') for b in data) def bytes_to_int(data): """ convert a sequence of bytes to an integer using big endian byte ordering :param data: byte sequence :return: integer value """ try: return int.from_bytes(data, byteorder='big') except: return data def int_to_bytes(int_value: int, length: int) -> bytes: """ convert an integer to a sequence of bytes using big endian byte ordering :param int_value: integer value to convert :param length: (optional) byte length :return: byte sequence """ if length == 1: fmt = "!B" elif length == 2: fmt = "!H" return pack(fmt, int_value) @asyncio.coroutine def read_or_raise(reader, n=-1): """ Read a given byte number from Stream. NoDataException is raised if read gives no data :param reader: reader adapter :param n: number of bytes to read :return: bytes read """ data = yield from reader.read(n) if not data: raise NoDataException("No more data") return data @asyncio.coroutine def decode_string(reader) -> bytes: """ Read a string from a reader and decode it according to MQTT string specification :param reader: Stream reader :return: UTF-8 string read from stream """ length_bytes = yield from read_or_raise(reader, 2) str_length = unpack("!H", length_bytes) if str_length[0]: byte_str = yield from read_or_raise(reader, str_length[0]) try: return byte_str.decode(encoding='utf-8') except: return str(byte_str) else: return '' @asyncio.coroutine def decode_data_with_length(reader) -> bytes: """ Read data from a reader. Data is prefixed with 2 bytes length :param reader: Stream reader :return: bytes read from stream (without length) """ length_bytes = yield from read_or_raise(reader, 2) bytes_length = unpack("!H", length_bytes) data = yield from read_or_raise(reader, bytes_length[0]) return data def encode_string(string: str) -> bytes: data = string.encode(encoding='utf-8') data_length = len(data) return int_to_bytes(data_length, 2) + data def encode_data_with_length(data: bytes) -> bytes: data_length = len(data) return int_to_bytes(data_length, 2) + data @asyncio.coroutine def decode_packet_id(reader) -> int: """ Read a packet ID as 2-bytes int from stream according to MQTT specification (2.3.1) :param reader: Stream reader :return: Packet ID """ packet_id_bytes = yield from read_or_raise(reader, 2) packet_id = unpack("!H", packet_id_bytes) return packet_id[0] def int_to_bytes_str(value: int) -> bytes: """ Converts a int value to a bytes array containing the numeric character. Ex: 123 -> b'123' :param value: int value to convert :return: bytes array """ return str(value).encode('utf-8') ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1579961068.0 hbmqtt-0.9.6/hbmqtt/errors.py0000644000175000017510000000102300000000000016112 0ustar00niconico00000000000000# Copyright (c) 2015 Nicolas JOUANIN # # See the file license.txt for copying permission. class HBMQTTException(Exception): """ HBMQTT base exception """ pass class MQTTException(Exception): """ Base class for all errors refering to MQTT specifications """ pass class CodecException(Exception): """ Exceptions thrown by packet encode/decode functions """ pass class NoDataException(Exception): """ Exceptions thrown by packet encode/decode functions """ pass ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1579961563.6934717 hbmqtt-0.9.6/hbmqtt/mqtt/0000755000175000017510000000000000000000000015215 5ustar00niconico00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1579961068.0 hbmqtt-0.9.6/hbmqtt/mqtt/__init__.py0000644000175000017510000000316100000000000017327 0ustar00niconico00000000000000# Copyright (c) 2015 Nicolas JOUANIN # # See the file license.txt for copying permission. from hbmqtt.errors import HBMQTTException from hbmqtt.mqtt.packet import ( CONNECT, CONNACK, PUBLISH, PUBACK, PUBREC, PUBREL, PUBCOMP, SUBSCRIBE, SUBACK, UNSUBSCRIBE, UNSUBACK, PINGREQ, PINGRESP, DISCONNECT, MQTTFixedHeader) from hbmqtt.mqtt.connect import ConnectPacket from hbmqtt.mqtt.connack import ConnackPacket from hbmqtt.mqtt.disconnect import DisconnectPacket from hbmqtt.mqtt.pingreq import PingReqPacket from hbmqtt.mqtt.pingresp import PingRespPacket from hbmqtt.mqtt.publish import PublishPacket from hbmqtt.mqtt.puback import PubackPacket from hbmqtt.mqtt.pubrec import PubrecPacket from hbmqtt.mqtt.pubrel import PubrelPacket from hbmqtt.mqtt.pubcomp import PubcompPacket from hbmqtt.mqtt.subscribe import SubscribePacket from hbmqtt.mqtt.suback import SubackPacket from hbmqtt.mqtt.unsubscribe import UnsubscribePacket from hbmqtt.mqtt.unsuback import UnsubackPacket packet_dict = { CONNECT: ConnectPacket, CONNACK: ConnackPacket, PUBLISH: PublishPacket, PUBACK: PubackPacket, PUBREC: PubrecPacket, PUBREL: PubrelPacket, PUBCOMP: PubcompPacket, SUBSCRIBE: SubscribePacket, SUBACK: SubackPacket, UNSUBSCRIBE: UnsubscribePacket, UNSUBACK: UnsubackPacket, PINGREQ: PingReqPacket, PINGRESP: PingRespPacket, DISCONNECT: DisconnectPacket } def packet_class(fixed_header: MQTTFixedHeader): try: cls = packet_dict[fixed_header.packet_type] return cls except KeyError: raise HBMQTTException("Unexpected packet Type '%s'" % fixed_header.packet_type) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1579961068.0 hbmqtt-0.9.6/hbmqtt/mqtt/connack.py0000644000175000017510000000533000000000000017204 0ustar00niconico00000000000000# Copyright (c) 2015 Nicolas JOUANIN # # See the file license.txt for copying permission. import asyncio from hbmqtt.mqtt.packet import CONNACK, MQTTPacket, MQTTFixedHeader, MQTTVariableHeader from hbmqtt.codecs import read_or_raise, bytes_to_int from hbmqtt.errors import HBMQTTException from hbmqtt.adapters import ReaderAdapter CONNECTION_ACCEPTED = 0x00 UNACCEPTABLE_PROTOCOL_VERSION = 0x01 IDENTIFIER_REJECTED = 0x02 SERVER_UNAVAILABLE = 0x03 BAD_USERNAME_PASSWORD = 0x04 NOT_AUTHORIZED = 0x05 class ConnackVariableHeader(MQTTVariableHeader): __slots__ = ('session_parent', 'return_code') def __init__(self, session_parent=None, return_code=None): super().__init__() self.session_parent = session_parent self.return_code = return_code @classmethod @asyncio.coroutine def from_stream(cls, reader: ReaderAdapter, fixed_header: MQTTFixedHeader): data = yield from read_or_raise(reader, 2) session_parent = data[0] & 0x01 return_code = bytes_to_int(data[1]) return cls(session_parent, return_code) def to_bytes(self): out = bytearray(2) # Connect acknowledge flags if self.session_parent: out[0] = 1 else: out[0] = 0 # return code out[1] = self.return_code return out def __repr__(self): return type(self).__name__ + '(session_parent={0}, return_code={1})'\ .format(hex(self.session_parent), hex(self.return_code)) class ConnackPacket(MQTTPacket): VARIABLE_HEADER = ConnackVariableHeader PAYLOAD = None @property def return_code(self): return self.variable_header.return_code @return_code.setter def return_code(self, return_code): self.variable_header.return_code = return_code @property def session_parent(self): return self.variable_header.session_parent @session_parent.setter def session_parent(self, session_parent): self.variable_header.session_parent = session_parent def __init__(self, fixed: MQTTFixedHeader=None, variable_header: ConnackVariableHeader=None, payload=None): if fixed is None: header = MQTTFixedHeader(CONNACK, 0x00) else: if fixed.packet_type is not CONNACK: raise HBMQTTException("Invalid fixed packet type %s for ConnackPacket init" % fixed.packet_type) header = fixed super().__init__(header) self.variable_header = variable_header self.payload = None @classmethod def build(cls, session_parent=None, return_code=None): v_header = ConnackVariableHeader(session_parent, return_code) packet = ConnackPacket(variable_header=v_header) return packet ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1579961068.0 hbmqtt-0.9.6/hbmqtt/mqtt/connect.py0000644000175000017510000002502700000000000017226 0ustar00niconico00000000000000# Copyright (c) 2015 Nicolas JOUANIN # # See the file license.txt for copying permission. import asyncio from hbmqtt.codecs import bytes_to_int, decode_data_with_length, decode_string, encode_data_with_length, encode_string, int_to_bytes, read_or_raise from hbmqtt.mqtt.packet import MQTTPacket, MQTTFixedHeader, CONNECT, MQTTVariableHeader, MQTTPayload from hbmqtt.errors import HBMQTTException, NoDataException from hbmqtt.adapters import ReaderAdapter from hbmqtt.utils import gen_client_id class ConnectVariableHeader(MQTTVariableHeader): __slots__ = ('proto_name', 'proto_level', 'flags', 'keep_alive') USERNAME_FLAG = 0x80 PASSWORD_FLAG = 0x40 WILL_RETAIN_FLAG = 0x20 WILL_FLAG = 0x04 WILL_QOS_MASK = 0x18 CLEAN_SESSION_FLAG = 0x02 RESERVED_FLAG = 0x01 def __init__(self, connect_flags=0x00, keep_alive=0, proto_name='MQTT', proto_level=0x04): super().__init__() self.proto_name = proto_name self.proto_level = proto_level self.flags = connect_flags self.keep_alive = keep_alive def __repr__(self): return "ConnectVariableHeader(proto_name={0}, proto_level={1}, flags={2}, keepalive={3})".format( self.proto_name, self.proto_level, hex(self.flags), self.keep_alive) def _set_flag(self, val, mask): if val: self.flags |= mask else: self.flags &= ~mask def _get_flag(self, mask): if self.flags & mask: return True else: return False @property def username_flag(self) -> bool: return self._get_flag(self.USERNAME_FLAG) @username_flag.setter def username_flag(self, val: bool): self._set_flag(val, self.USERNAME_FLAG) @property def password_flag(self) -> bool: return self._get_flag(self.PASSWORD_FLAG) @password_flag.setter def password_flag(self, val: bool): self._set_flag(val, self.PASSWORD_FLAG) @property def will_retain_flag(self) -> bool: return self._get_flag(self.WILL_RETAIN_FLAG) @will_retain_flag.setter def will_retain_flag(self, val: bool): self._set_flag(val, self.WILL_RETAIN_FLAG) @property def will_flag(self) -> bool: return self._get_flag(self.WILL_FLAG) @will_flag.setter def will_flag(self, val: bool): self._set_flag(val, self.WILL_FLAG) @property def clean_session_flag(self) -> bool: return self._get_flag(self.CLEAN_SESSION_FLAG) @clean_session_flag.setter def clean_session_flag(self, val: bool): self._set_flag(val, self.CLEAN_SESSION_FLAG) @property def reserved_flag(self) -> bool: return self._get_flag(self.RESERVED_FLAG) @property def will_qos(self): return (self.flags & 0x18) >> 3 @will_qos.setter def will_qos(self, val: int): self.flags &= 0xe7 # Reset QOS flags self.flags |= (val << 3) @classmethod @asyncio.coroutine def from_stream(cls, reader: ReaderAdapter, fixed_header: MQTTFixedHeader): # protocol name protocol_name = yield from decode_string(reader) # protocol level protocol_level_byte = yield from read_or_raise(reader, 1) protocol_level = bytes_to_int(protocol_level_byte) # flags flags_byte = yield from read_or_raise(reader, 1) flags = bytes_to_int(flags_byte) # keep-alive keep_alive_byte = yield from read_or_raise(reader, 2) keep_alive = bytes_to_int(keep_alive_byte) return cls(flags, keep_alive, protocol_name, protocol_level) def to_bytes(self): out = bytearray() # Protocol name out.extend(encode_string(self.proto_name)) # Protocol level out.append(self.proto_level) # flags out.append(self.flags) # keep alive out.extend(int_to_bytes(self.keep_alive, 2)) return out class ConnectPayload(MQTTPayload): __slots__ = ( 'client_id', 'will_topic', 'will_message', 'username', 'password', 'client_id_is_random', ) def __init__(self, client_id=None, will_topic=None, will_message=None, username=None, password=None): super().__init__() self.client_id_is_random = False self.client_id = client_id self.will_topic = will_topic self.will_message = will_message self.username = username self.password = password def __repr__(self): return "ConnectVariableHeader(client_id={0}, will_topic={1}, will_message={2}, username={3}, password={4})".\ format(self.client_id, self.will_topic, self.will_message, self.username, self.password) @classmethod @asyncio.coroutine def from_stream(cls, reader: ReaderAdapter, fixed_header: MQTTFixedHeader, variable_header: ConnectVariableHeader): payload = cls() # Client identifier try: payload.client_id = yield from decode_string(reader) except NoDataException: payload.client_id = None if (payload.client_id is None or payload.client_id == ""): # A Server MAY allow a Client to supply a ClientId that has a length of zero bytes # [MQTT-3.1.3-6] payload.client_id = gen_client_id() # indicator to trow exception in case CLEAN_SESSION_FLAG is set to False payload.client_id_is_random = True # Read will topic, username and password if variable_header.will_flag: try: payload.will_topic = yield from decode_string(reader) payload.will_message = yield from decode_data_with_length(reader) except NoDataException: payload.will_topic = None payload.will_message = None if variable_header.username_flag: try: payload.username = yield from decode_string(reader) except NoDataException: payload.username = None if variable_header.password_flag: try: payload.password = yield from decode_string(reader) except NoDataException: payload.password = None return payload def to_bytes(self, fixed_header: MQTTFixedHeader, variable_header: ConnectVariableHeader): out = bytearray() # Client identifier out.extend(encode_string(self.client_id)) # Will topic / message if variable_header.will_flag: out.extend(encode_string(self.will_topic)) out.extend(encode_data_with_length(self.will_message)) # username if variable_header.username_flag: out.extend(encode_string(self.username)) # password if variable_header.password_flag: out.extend(encode_string(self.password)) return out class ConnectPacket(MQTTPacket): VARIABLE_HEADER = ConnectVariableHeader PAYLOAD = ConnectPayload @property def proto_name(self): return self.variable_header.proto_name @proto_name.setter def proto_name(self, name: str): self.variable_header.proto_name = name @property def proto_level(self): return self.variable_header.proto_level @proto_level.setter def proto_level(self, level): self.variable_header.proto_level = level @property def username_flag(self): return self.variable_header.username_flag @username_flag.setter def username_flag(self, flag): self.variable_header.username_flag = flag @property def password_flag(self): return self.variable_header.password_flag @password_flag.setter def password_flag(self, flag): self.variable_header.password_flag = flag @property def clean_session_flag(self): return self.variable_header.clean_session_flag @clean_session_flag.setter def clean_session_flag(self, flag): self.variable_header.clean_session_flag = flag @property def will_retain_flag(self): return self.variable_header.will_retain_flag @will_retain_flag.setter def will_retain_flag(self, flag): self.variable_header.will_retain_flag = flag @property def will_qos(self): return self.variable_header.will_qos @will_qos.setter def will_qos(self, flag): self.variable_header.will_qos = flag @property def will_flag(self): return self.variable_header.will_flag @will_flag.setter def will_flag(self, flag): self.variable_header.will_flag = flag @property def reserved_flag(self): return self.variable_header.reserved_flag @reserved_flag.setter def reserved_flag(self, flag): self.variable_header.reserved_flag = flag @property def client_id(self): return self.payload.client_id @client_id.setter def client_id(self, client_id): self.payload.client_id = client_id @property def client_id_is_random(self) -> bool: return self.payload.client_id_is_random @client_id_is_random.setter def client_id_is_random(self, client_id_is_random: bool): self.payload.client_id_is_random = client_id_is_random @property def will_topic(self): return self.payload.will_topic @will_topic.setter def will_topic(self, will_topic): self.payload.will_topic = will_topic @property def will_message(self): return self.payload.will_message @will_message.setter def will_message(self, will_message): self.payload.will_message = will_message @property def username(self): return self.payload.username @username.setter def username(self, username): self.payload.username = username @property def password(self): return self.payload.password @password.setter def password(self, password): self.payload.password = password @property def keep_alive(self): return self.variable_header.keep_alive @keep_alive.setter def keep_alive(self, keep_alive): self.variable_header.keep_alive = keep_alive def __init__(self, fixed: MQTTFixedHeader=None, vh: ConnectVariableHeader=None, payload: ConnectPayload=None): if fixed is None: header = MQTTFixedHeader(CONNECT, 0x00) else: if fixed.packet_type is not CONNECT: raise HBMQTTException("Invalid fixed packet type %s for ConnectPacket init" % fixed.packet_type) header = fixed super().__init__(header) self.variable_header = vh self.payload = payload ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1579961068.0 hbmqtt-0.9.6/hbmqtt/mqtt/constants.py0000644000175000017510000000020200000000000017575 0ustar00niconico00000000000000# Copyright (c) 2015 Nicolas JOUANIN # # See the file license.txt for copying permission. QOS_0 = 0x00 QOS_1 = 0x01 QOS_2 = 0x02 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1579961068.0 hbmqtt-0.9.6/hbmqtt/mqtt/disconnect.py0000644000175000017510000000133000000000000017715 0ustar00niconico00000000000000# Copyright (c) 2015 Nicolas JOUANIN # # See the file license.txt for copying permission. from hbmqtt.mqtt.packet import MQTTPacket, MQTTFixedHeader, DISCONNECT from hbmqtt.errors import HBMQTTException class DisconnectPacket(MQTTPacket): VARIABLE_HEADER = None PAYLOAD = None def __init__(self, fixed: MQTTFixedHeader=None): if fixed is None: header = MQTTFixedHeader(DISCONNECT, 0x00) else: if fixed.packet_type is not DISCONNECT: raise HBMQTTException("Invalid fixed packet type %s for DisconnectPacket init" % fixed.packet_type) header = fixed super().__init__(header) self.variable_header = None self.payload = None ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1579961068.0 hbmqtt-0.9.6/hbmqtt/mqtt/packet.py0000644000175000017510000001700700000000000017043 0ustar00niconico00000000000000# Copyright (c) 2015 Nicolas JOUANIN # # See the file license.txt for copying permission. import asyncio from hbmqtt.codecs import bytes_to_hex_str, decode_packet_id, int_to_bytes, read_or_raise from hbmqtt.errors import CodecException, MQTTException, NoDataException from hbmqtt.adapters import ReaderAdapter, WriterAdapter from datetime import datetime from struct import unpack RESERVED_0 = 0x00 CONNECT = 0x01 CONNACK = 0x02 PUBLISH = 0x03 PUBACK = 0x04 PUBREC = 0x05 PUBREL = 0x06 PUBCOMP = 0x07 SUBSCRIBE = 0x08 SUBACK = 0x09 UNSUBSCRIBE = 0x0a UNSUBACK = 0x0b PINGREQ = 0x0c PINGRESP = 0x0d DISCONNECT = 0x0e RESERVED_15 = 0x0f class MQTTFixedHeader: __slots__ = ('packet_type', 'remaining_length', 'flags') def __init__(self, packet_type, flags=0, length=0): self.packet_type = packet_type self.remaining_length = length self.flags = flags def to_bytes(self): def encode_remaining_length(length: int): encoded = bytearray() while True: length_byte = length % 0x80 length //= 0x80 if length > 0: length_byte |= 0x80 encoded.append(length_byte) if length <= 0: break return encoded out = bytearray() packet_type = 0 try: packet_type = (self.packet_type << 4) | self.flags out.append(packet_type) except OverflowError: raise CodecException('packet_type encoding exceed 1 byte length: value=%d', packet_type) encoded_length = encode_remaining_length(self.remaining_length) out.extend(encoded_length) return out @asyncio.coroutine def to_stream(self, writer: WriterAdapter): writer.write(self.to_bytes()) @property def bytes_length(self): return len(self.to_bytes()) @classmethod @asyncio.coroutine def from_stream(cls, reader: ReaderAdapter): """ Read and decode MQTT message fixed header from stream :return: FixedHeader instance """ @asyncio.coroutine def decode_remaining_length(): """ Decode message length according to MQTT specifications :return: """ multiplier = 1 value = 0 buffer = bytearray() while True: encoded_byte = yield from reader.read(1) int_byte = unpack('!B', encoded_byte) buffer.append(int_byte[0]) value += (int_byte[0] & 0x7f) * multiplier if (int_byte[0] & 0x80) == 0: break else: multiplier *= 128 if multiplier > 128 * 128 * 128: raise MQTTException("Invalid remaining length bytes:%s, packet_type=%d" % (bytes_to_hex_str(buffer), msg_type)) return value try: byte1 = yield from read_or_raise(reader, 1) int1 = unpack('!B', byte1) msg_type = (int1[0] & 0xf0) >> 4 flags = int1[0] & 0x0f remain_length = yield from decode_remaining_length() return cls(msg_type, flags, remain_length) except NoDataException: return None def __repr__(self): return type(self).__name__ + '(length={0}, flags={1})'.\ format(self.remaining_length, hex(self.flags)) class MQTTVariableHeader: def __init__(self): pass @asyncio.coroutine def to_stream(self, writer: asyncio.StreamWriter): writer.write(self.to_bytes()) yield from writer.drain() def to_bytes(self) -> bytes: """ Serialize header data to a byte array conforming to MQTT protocol :return: serialized data """ @property def bytes_length(self): return len(self.to_bytes()) @classmethod @asyncio.coroutine def from_stream(cls, reader: asyncio.StreamReader, fixed_header: MQTTFixedHeader): pass class PacketIdVariableHeader(MQTTVariableHeader): __slots__ = ('packet_id',) def __init__(self, packet_id): super().__init__() self.packet_id = packet_id def to_bytes(self): out = b'' out += int_to_bytes(self.packet_id, 2) return out @classmethod @asyncio.coroutine def from_stream(cls, reader: ReaderAdapter, fixed_header: MQTTFixedHeader): packet_id = yield from decode_packet_id(reader) return cls(packet_id) def __repr__(self): return type(self).__name__ + '(packet_id={0})'.format(self.packet_id) class MQTTPayload: def __init__(self): pass @asyncio.coroutine def to_stream(self, writer: asyncio.StreamWriter): writer.write(self.to_bytes()) yield from writer.drain() def to_bytes(self, fixed_header: MQTTFixedHeader, variable_header: MQTTVariableHeader): pass @classmethod @asyncio.coroutine def from_stream(cls, reader: asyncio.StreamReader, fixed_header: MQTTFixedHeader, variable_header: MQTTVariableHeader): pass class MQTTPacket: __slots__ = ('fixed_header', 'variable_header', 'payload', 'protocol_ts') FIXED_HEADER = MQTTFixedHeader VARIABLE_HEADER = None PAYLOAD = None def __init__(self, fixed: MQTTFixedHeader, variable_header: MQTTVariableHeader=None, payload: MQTTPayload=None): self.fixed_header = fixed self.variable_header = variable_header self.payload = payload self.protocol_ts = None @asyncio.coroutine def to_stream(self, writer: asyncio.StreamWriter): writer.write(self.to_bytes()) yield from writer.drain() self.protocol_ts = datetime.now() def to_bytes(self) -> bytes: if self.variable_header: variable_header_bytes = self.variable_header.to_bytes() else: variable_header_bytes = b'' if self.payload: payload_bytes = self.payload.to_bytes(self.fixed_header, self.variable_header) else: payload_bytes = b'' self.fixed_header.remaining_length = len(variable_header_bytes) + len(payload_bytes) fixed_header_bytes = self.fixed_header.to_bytes() return fixed_header_bytes + variable_header_bytes + payload_bytes @classmethod @asyncio.coroutine def from_stream(cls, reader: ReaderAdapter, fixed_header=None, variable_header=None): if fixed_header is None: fixed_header = yield from cls.FIXED_HEADER.from_stream(reader) if cls.VARIABLE_HEADER: if variable_header is None: variable_header = yield from cls.VARIABLE_HEADER.from_stream(reader, fixed_header) else: variable_header = None if cls.PAYLOAD: payload = yield from cls.PAYLOAD.from_stream(reader, fixed_header, variable_header) else: payload = None if fixed_header and not variable_header and not payload: instance = cls(fixed_header) elif fixed_header and not payload: instance = cls(fixed_header, variable_header) else: instance = cls(fixed_header, variable_header, payload) instance.protocol_ts = datetime.now() return instance @property def bytes_length(self): return len(self.to_bytes()) def __repr__(self): return type(self).__name__ + '(ts={0!s}, fixed={1!r}, variable={2!r}, payload={3!r})'.\ format(self.protocol_ts, self.fixed_header, self.variable_header, self.payload) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1579961068.0 hbmqtt-0.9.6/hbmqtt/mqtt/pingreq.py0000644000175000017510000000131100000000000017230 0ustar00niconico00000000000000# Copyright (c) 2015 Nicolas JOUANIN # # See the file license.txt for copying permission. from hbmqtt.mqtt.packet import MQTTPacket, MQTTFixedHeader, PINGREQ from hbmqtt.errors import HBMQTTException class PingReqPacket(MQTTPacket): VARIABLE_HEADER = None PAYLOAD = None def __init__(self, fixed: MQTTFixedHeader=None): if fixed is None: header = MQTTFixedHeader(PINGREQ, 0x00) else: if fixed.packet_type is not PINGREQ: raise HBMQTTException("Invalid fixed packet type %s for PingReqPacket init" % fixed.packet_type) header = fixed super().__init__(header) self.variable_header = None self.payload = None ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1579961068.0 hbmqtt-0.9.6/hbmqtt/mqtt/pingresp.py0000644000175000017510000000141100000000000017413 0ustar00niconico00000000000000# Copyright (c) 2015 Nicolas JOUANIN # # See the file license.txt for copying permission. from hbmqtt.mqtt.packet import MQTTPacket, MQTTFixedHeader, PINGRESP from hbmqtt.errors import HBMQTTException class PingRespPacket(MQTTPacket): VARIABLE_HEADER = None PAYLOAD = None def __init__(self, fixed: MQTTFixedHeader=None): if fixed is None: header = MQTTFixedHeader(PINGRESP, 0x00) else: if fixed.packet_type is not PINGRESP: raise HBMQTTException("Invalid fixed packet type %s for PingRespPacket init" % fixed.packet_type) header = fixed super().__init__(header) self.variable_header = None self.payload = None @classmethod def build(cls): return cls() ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1579961563.6934717 hbmqtt-0.9.6/hbmqtt/mqtt/protocol/0000755000175000017510000000000000000000000017056 5ustar00niconico00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1579961068.0 hbmqtt-0.9.6/hbmqtt/mqtt/protocol/__init__.py0000644000175000017510000000000000000000000021155 0ustar00niconico00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1579961068.0 hbmqtt-0.9.6/hbmqtt/mqtt/protocol/broker_handler.py0000644000175000017510000002116400000000000022415 0ustar00niconico00000000000000# Copyright (c) 2015 Nicolas JOUANIN # # See the file license.txt for copying permission. import asyncio from asyncio import futures, Queue from hbmqtt.mqtt.protocol.handler import ProtocolHandler from hbmqtt.mqtt.connack import ( CONNECTION_ACCEPTED, UNACCEPTABLE_PROTOCOL_VERSION, IDENTIFIER_REJECTED, BAD_USERNAME_PASSWORD, NOT_AUTHORIZED, ConnackPacket) from hbmqtt.mqtt.connect import ConnectPacket from hbmqtt.mqtt.pingreq import PingReqPacket from hbmqtt.mqtt.pingresp import PingRespPacket from hbmqtt.mqtt.subscribe import SubscribePacket from hbmqtt.mqtt.suback import SubackPacket from hbmqtt.mqtt.unsubscribe import UnsubscribePacket from hbmqtt.mqtt.unsuback import UnsubackPacket from hbmqtt.utils import format_client_message from hbmqtt.session import Session from hbmqtt.plugins.manager import PluginManager from hbmqtt.adapters import ReaderAdapter, WriterAdapter from hbmqtt.errors import MQTTException from .handler import EVENT_MQTT_PACKET_RECEIVED, EVENT_MQTT_PACKET_SENT class BrokerProtocolHandler(ProtocolHandler): def __init__(self, plugins_manager: PluginManager, session: Session=None, loop=None): super().__init__(plugins_manager, session, loop) self._disconnect_waiter = None self._pending_subscriptions = Queue(loop=self._loop) self._pending_unsubscriptions = Queue(loop=self._loop) @asyncio.coroutine def start(self): yield from super().start() if self._disconnect_waiter is None: self._disconnect_waiter = futures.Future(loop=self._loop) @asyncio.coroutine def stop(self): yield from super().stop() if self._disconnect_waiter is not None and not self._disconnect_waiter.done(): self._disconnect_waiter.set_result(None) @asyncio.coroutine def wait_disconnect(self): return (yield from self._disconnect_waiter) def handle_write_timeout(self): pass def handle_read_timeout(self): if self._disconnect_waiter is not None and not self._disconnect_waiter.done(): self._disconnect_waiter.set_result(None) @asyncio.coroutine def handle_disconnect(self, disconnect): self.logger.debug("Client disconnecting") if self._disconnect_waiter and not self._disconnect_waiter.done(): self.logger.debug("Setting waiter result to %r" % disconnect) self._disconnect_waiter.set_result(disconnect) @asyncio.coroutine def handle_connection_closed(self): yield from self.handle_disconnect(None) @asyncio.coroutine def handle_connect(self, connect: ConnectPacket): # Broker handler shouldn't received CONNECT message during messages handling # as CONNECT messages are managed by the broker on client connection self.logger.error('%s [MQTT-3.1.0-2] %s : CONNECT message received during messages handling' % (self.session.client_id, format_client_message(self.session))) if self._disconnect_waiter is not None and not self._disconnect_waiter.done(): self._disconnect_waiter.set_result(None) @asyncio.coroutine def handle_pingreq(self, pingreq: PingReqPacket): yield from self._send_packet(PingRespPacket.build()) @asyncio.coroutine def handle_subscribe(self, subscribe: SubscribePacket): subscription = {'packet_id': subscribe.variable_header.packet_id, 'topics': subscribe.payload.topics} yield from self._pending_subscriptions.put(subscription) @asyncio.coroutine def handle_unsubscribe(self, unsubscribe: UnsubscribePacket): unsubscription = {'packet_id': unsubscribe.variable_header.packet_id, 'topics': unsubscribe.payload.topics} yield from self._pending_unsubscriptions.put(unsubscription) @asyncio.coroutine def get_next_pending_subscription(self): subscription = yield from self._pending_subscriptions.get() return subscription @asyncio.coroutine def get_next_pending_unsubscription(self): unsubscription = yield from self._pending_unsubscriptions.get() return unsubscription @asyncio.coroutine def mqtt_acknowledge_subscription(self, packet_id, return_codes): suback = SubackPacket.build(packet_id, return_codes) yield from self._send_packet(suback) @asyncio.coroutine def mqtt_acknowledge_unsubscription(self, packet_id): unsuback = UnsubackPacket.build(packet_id) yield from self._send_packet(unsuback) @asyncio.coroutine def mqtt_connack_authorize(self, authorize: bool): if authorize: connack = ConnackPacket.build(self.session.parent, CONNECTION_ACCEPTED) else: connack = ConnackPacket.build(self.session.parent, NOT_AUTHORIZED) yield from self._send_packet(connack) @classmethod @asyncio.coroutine def init_from_connect(cls, reader: ReaderAdapter, writer: WriterAdapter, plugins_manager, loop=None): """ :param reader: :param writer: :param plugins_manager: :param loop: :return: """ remote_address, remote_port = writer.get_peer_info() connect = yield from ConnectPacket.from_stream(reader) yield from plugins_manager.fire_event(EVENT_MQTT_PACKET_RECEIVED, packet=connect) #this shouldn't be required anymore since broker generates for each client a random client_id if not provided #[MQTT-3.1.3-6] if connect.payload.client_id is None: raise MQTTException('[[MQTT-3.1.3-3]] : Client identifier must be present') if connect.variable_header.will_flag: if connect.payload.will_topic is None or connect.payload.will_message is None: raise MQTTException('will flag set, but will topic/message not present in payload') if connect.variable_header.reserved_flag: raise MQTTException('[MQTT-3.1.2-3] CONNECT reserved flag must be set to 0') if connect.proto_name != "MQTT": raise MQTTException('[MQTT-3.1.2-1] Incorrect protocol name: "%s"' % connect.proto_name) connack = None error_msg = None if connect.proto_level != 4: # only MQTT 3.1.1 supported error_msg = 'Invalid protocol from %s: %d' % ( format_client_message(address=remote_address, port=remote_port), connect.proto_level) connack = ConnackPacket.build(0, UNACCEPTABLE_PROTOCOL_VERSION) # [MQTT-3.2.2-4] session_parent=0 elif not connect.username_flag and connect.password_flag: connack = ConnackPacket.build(0, BAD_USERNAME_PASSWORD) # [MQTT-3.1.2-22] elif connect.username_flag and not connect.password_flag: connack = ConnackPacket.build(0, BAD_USERNAME_PASSWORD) # [MQTT-3.1.2-22] elif connect.username_flag and connect.username is None: error_msg = 'Invalid username from %s' % ( format_client_message(address=remote_address, port=remote_port)) connack = ConnackPacket.build(0, BAD_USERNAME_PASSWORD) # [MQTT-3.2.2-4] session_parent=0 elif connect.password_flag and connect.password is None: error_msg = 'Invalid password %s' % (format_client_message(address=remote_address, port=remote_port)) connack = ConnackPacket.build(0, BAD_USERNAME_PASSWORD) # [MQTT-3.2.2-4] session_parent=0 elif connect.clean_session_flag is False and (connect.payload.client_id_is_random): error_msg = '[MQTT-3.1.3-8] [MQTT-3.1.3-9] %s: No client Id provided (cleansession=0)' % ( format_client_message(address=remote_address, port=remote_port)) connack = ConnackPacket.build(0, IDENTIFIER_REJECTED) if connack is not None: yield from plugins_manager.fire_event(EVENT_MQTT_PACKET_SENT, packet=connack) yield from connack.to_stream(writer) yield from writer.close() raise MQTTException(error_msg) incoming_session = Session(loop) incoming_session.client_id = connect.client_id incoming_session.clean_session = connect.clean_session_flag incoming_session.will_flag = connect.will_flag incoming_session.will_retain = connect.will_retain_flag incoming_session.will_qos = connect.will_qos incoming_session.will_topic = connect.will_topic incoming_session.will_message = connect.will_message incoming_session.username = connect.username incoming_session.password = connect.password if connect.keep_alive > 0: incoming_session.keep_alive = connect.keep_alive else: incoming_session.keep_alive = 0 handler = cls(plugins_manager, loop=loop) return handler, incoming_session ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1579961068.0 hbmqtt-0.9.6/hbmqtt/mqtt/protocol/client_handler.py0000644000175000017510000001445700000000000022416 0ustar00niconico00000000000000# Copyright (c) 2015 Nicolas JOUANIN # # See the file license.txt for copying permission. import asyncio from asyncio import futures import sys from hbmqtt.mqtt.protocol.handler import ProtocolHandler, EVENT_MQTT_PACKET_RECEIVED from hbmqtt.mqtt.disconnect import DisconnectPacket from hbmqtt.mqtt.pingreq import PingReqPacket from hbmqtt.mqtt.pingresp import PingRespPacket from hbmqtt.mqtt.subscribe import SubscribePacket from hbmqtt.mqtt.suback import SubackPacket from hbmqtt.mqtt.unsubscribe import UnsubscribePacket from hbmqtt.mqtt.unsuback import UnsubackPacket from hbmqtt.mqtt.connect import ConnectVariableHeader, ConnectPayload, ConnectPacket from hbmqtt.mqtt.connack import ConnackPacket from hbmqtt.session import Session from hbmqtt.plugins.manager import PluginManager class ClientProtocolHandler(ProtocolHandler): def __init__(self, plugins_manager: PluginManager, session: Session=None, loop=None): super().__init__(plugins_manager, session, loop=loop) self._ping_task = None self._pingresp_queue = asyncio.Queue(loop=self._loop) self._subscriptions_waiter = dict() self._unsubscriptions_waiter = dict() self._disconnect_waiter = None @asyncio.coroutine def start(self): yield from super().start() if self._disconnect_waiter is None: self._disconnect_waiter = futures.Future(loop=self._loop) @asyncio.coroutine def stop(self): yield from super().stop() if self._ping_task: try: self.logger.debug("Cancel ping task") self._ping_task.cancel() except BaseException: pass if not self._disconnect_waiter.done(): self._disconnect_waiter.cancel() self._disconnect_waiter = None def _build_connect_packet(self): vh = ConnectVariableHeader() payload = ConnectPayload() vh.keep_alive = self.session.keep_alive vh.clean_session_flag = self.session.clean_session vh.will_retain_flag = self.session.will_retain payload.client_id = self.session.client_id if self.session.username: vh.username_flag = True payload.username = self.session.username else: vh.username_flag = False if self.session.password: vh.password_flag = True payload.password = self.session.password else: vh.password_flag = False if self.session.will_flag: vh.will_flag = True vh.will_qos = self.session.will_qos payload.will_message = self.session.will_message payload.will_topic = self.session.will_topic else: vh.will_flag = False packet = ConnectPacket(vh=vh, payload=payload) return packet @asyncio.coroutine def mqtt_connect(self): connect_packet = self._build_connect_packet() yield from self._send_packet(connect_packet) connack = yield from ConnackPacket.from_stream(self.reader) yield from self.plugins_manager.fire_event(EVENT_MQTT_PACKET_RECEIVED, packet=connack, session=self.session) return connack.return_code def handle_write_timeout(self): try: if not self._ping_task: self.logger.debug("Scheduling Ping") self._ping_task = asyncio.ensure_future(self.mqtt_ping()) except BaseException as be: self.logger.debug("Exception ignored in ping task: %r" % be) def handle_read_timeout(self): pass @asyncio.coroutine def mqtt_subscribe(self, topics, packet_id): """ :param topics: array of topics [{'filter':'/a/b', 'qos': 0x00}, ...] :return: """ # Build and send SUBSCRIBE message subscribe = SubscribePacket.build(topics, packet_id) yield from self._send_packet(subscribe) # Wait for SUBACK is received waiter = futures.Future(loop=self._loop) self._subscriptions_waiter[subscribe.variable_header.packet_id] = waiter return_codes = yield from waiter del self._subscriptions_waiter[subscribe.variable_header.packet_id] return return_codes @asyncio.coroutine def handle_suback(self, suback: SubackPacket): packet_id = suback.variable_header.packet_id try: waiter = self._subscriptions_waiter.get(packet_id) waiter.set_result(suback.payload.return_codes) except KeyError as ke: self.logger.warning("Received SUBACK for unknown pending subscription with Id: %s" % packet_id) @asyncio.coroutine def mqtt_unsubscribe(self, topics, packet_id): """ :param topics: array of topics ['/a/b', ...] :return: """ unsubscribe = UnsubscribePacket.build(topics, packet_id) yield from self._send_packet(unsubscribe) waiter = futures.Future(loop=self._loop) self._unsubscriptions_waiter[unsubscribe.variable_header.packet_id] = waiter yield from waiter del self._unsubscriptions_waiter[unsubscribe.variable_header.packet_id] @asyncio.coroutine def handle_unsuback(self, unsuback: UnsubackPacket): packet_id = unsuback.variable_header.packet_id try: waiter = self._unsubscriptions_waiter.get(packet_id) waiter.set_result(None) except KeyError as ke: self.logger.warning("Received UNSUBACK for unknown pending subscription with Id: %s" % packet_id) @asyncio.coroutine def mqtt_disconnect(self): disconnect_packet = DisconnectPacket() yield from self._send_packet(disconnect_packet) @asyncio.coroutine def mqtt_ping(self): ping_packet = PingReqPacket() yield from self._send_packet(ping_packet) resp = yield from self._pingresp_queue.get() if self._ping_task: self._ping_task = None return resp @asyncio.coroutine def handle_pingresp(self, pingresp: PingRespPacket): yield from self._pingresp_queue.put(pingresp) @asyncio.coroutine def handle_connection_closed(self): self.logger.debug("Broker closed connection") if not self._disconnect_waiter.done(): self._disconnect_waiter.set_result(None) @asyncio.coroutine def wait_disconnect(self): yield from self._disconnect_waiter ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1579961068.0 hbmqtt-0.9.6/hbmqtt/mqtt/protocol/handler.py0000644000175000017510000006464500000000000021064 0ustar00niconico00000000000000# Copyright (c) 2015 Nicolas JOUANIN # # See the file license.txt for copying permission. import logging import collections import itertools import asyncio from asyncio import InvalidStateError from hbmqtt.mqtt import packet_class from hbmqtt.mqtt.connack import ConnackPacket from hbmqtt.mqtt.connect import ConnectPacket from hbmqtt.mqtt.packet import ( RESERVED_0, CONNECT, CONNACK, PUBLISH, PUBACK, PUBREC, PUBREL, PUBCOMP, SUBSCRIBE, SUBACK, UNSUBSCRIBE, UNSUBACK, PINGREQ, PINGRESP, DISCONNECT, RESERVED_15, MQTTFixedHeader) from hbmqtt.mqtt.pingresp import PingRespPacket from hbmqtt.mqtt.pingreq import PingReqPacket from hbmqtt.mqtt.publish import PublishPacket from hbmqtt.mqtt.pubrel import PubrelPacket from hbmqtt.mqtt.puback import PubackPacket from hbmqtt.mqtt.pubrec import PubrecPacket from hbmqtt.mqtt.pubcomp import PubcompPacket from hbmqtt.mqtt.suback import SubackPacket from hbmqtt.mqtt.subscribe import SubscribePacket from hbmqtt.mqtt.unsubscribe import UnsubscribePacket from hbmqtt.mqtt.unsuback import UnsubackPacket from hbmqtt.mqtt.disconnect import DisconnectPacket from hbmqtt.adapters import ReaderAdapter, WriterAdapter from hbmqtt.session import Session, OutgoingApplicationMessage, IncomingApplicationMessage, INCOMING, OUTGOING from hbmqtt.mqtt.constants import QOS_0, QOS_1, QOS_2 from hbmqtt.plugins.manager import PluginManager from hbmqtt.errors import HBMQTTException, MQTTException, NoDataException EVENT_MQTT_PACKET_SENT = 'mqtt_packet_sent' EVENT_MQTT_PACKET_RECEIVED = 'mqtt_packet_received' class ProtocolHandlerException(BaseException): pass class ProtocolHandler: """ Class implementing the MQTT communication protocol using asyncio features """ def __init__(self, plugins_manager: PluginManager, session: Session=None, loop=None): self.logger = logging.getLogger(__name__) if session: self._init_session(session) else: self.session = None self.reader = None self.writer = None self.plugins_manager = plugins_manager if loop is None: self._loop = asyncio.get_event_loop() else: self._loop = loop self._reader_task = None self._keepalive_task = None self._reader_ready = None self._reader_stopped = asyncio.Event(loop=self._loop) self._puback_waiters = dict() self._pubrec_waiters = dict() self._pubrel_waiters = dict() self._pubcomp_waiters = dict() self._write_lock = asyncio.Lock(loop=self._loop) def _init_session(self, session: Session): assert session log = logging.getLogger(__name__) self.session = session self.logger = logging.LoggerAdapter(log, {'client_id': self.session.client_id}) self.keepalive_timeout = self.session.keep_alive if self.keepalive_timeout <= 0: self.keepalive_timeout = None def attach(self, session, reader: ReaderAdapter, writer: WriterAdapter): if self.session: raise ProtocolHandlerException("Handler is already attached to a session") self._init_session(session) self.reader = reader self.writer = writer def detach(self): self.session = None self.reader = None self.writer = None def _is_attached(self): if self.session: return True else: return False @asyncio.coroutine def start(self): if not self._is_attached(): raise ProtocolHandlerException("Handler is not attached to a stream") self._reader_ready = asyncio.Event(loop=self._loop) self._reader_task = asyncio.Task(self._reader_loop(), loop=self._loop) yield from asyncio.wait([self._reader_ready.wait()], loop=self._loop) if self.keepalive_timeout: self._keepalive_task = self._loop.call_later(self.keepalive_timeout, self.handle_write_timeout) self.logger.debug("Handler tasks started") yield from self._retry_deliveries() self.logger.debug("Handler ready") @asyncio.coroutine def stop(self): # Stop messages flow waiter self._stop_waiters() if self._keepalive_task: self._keepalive_task.cancel() self.logger.debug("waiting for tasks to be stopped") if not self._reader_task.done(): self._reader_task.cancel() yield from asyncio.wait( [self._reader_stopped.wait()], loop=self._loop) self.logger.debug("closing writer") try: yield from self.writer.close() except Exception as e: self.logger.debug("Handler writer close failed: %s" % e) def _stop_waiters(self): self.logger.debug("Stopping %d puback waiters" % len(self._puback_waiters)) self.logger.debug("Stopping %d pucomp waiters" % len(self._pubcomp_waiters)) self.logger.debug("Stopping %d purec waiters" % len(self._pubrec_waiters)) self.logger.debug("Stopping %d purel waiters" % len(self._pubrel_waiters)) for waiter in itertools.chain( self._puback_waiters.values(), self._pubcomp_waiters.values(), self._pubrec_waiters.values(), self._pubrel_waiters.values()): waiter.cancel() @asyncio.coroutine def _retry_deliveries(self): """ Handle [MQTT-4.4.0-1] by resending PUBLISH and PUBREL messages for pending out messages :return: """ self.logger.debug("Begin messages delivery retries") tasks = [] for message in itertools.chain(self.session.inflight_in.values(), self.session.inflight_out.values()): tasks.append(asyncio.wait_for(self._handle_message_flow(message), 10, loop=self._loop)) if tasks: done, pending = yield from asyncio.wait(tasks, loop=self._loop) self.logger.debug("%d messages redelivered" % len(done)) self.logger.debug("%d messages not redelivered due to timeout" % len(pending)) self.logger.debug("End messages delivery retries") @asyncio.coroutine def mqtt_publish(self, topic, data, qos, retain, ack_timeout=None): """ Sends a MQTT publish message and manages messages flows. This methods doesn't return until the message has been acknowledged by receiver or timeout occur :param topic: MQTT topic to publish :param data: data to send on topic :param qos: quality of service to use for message flow. Can be QOS_0, QOS_1 or QOS_2 :param retain: retain message flag :param ack_timeout: acknowledge timeout. If set, this method will return a TimeOut error if the acknowledgment is not completed before ack_timeout second :return: ApplicationMessage used during inflight operations """ if qos in (QOS_1, QOS_2): packet_id = self.session.next_packet_id if packet_id in self.session.inflight_out: raise HBMQTTException("A message with the same packet ID '%d' is already in flight" % packet_id) else: packet_id = None message = OutgoingApplicationMessage(packet_id, topic, qos, data, retain) # Handle message flow if ack_timeout is not None and ack_timeout > 0: yield from asyncio.wait_for(self._handle_message_flow(message), ack_timeout, loop=self._loop) else: yield from self._handle_message_flow(message) return message @asyncio.coroutine def _handle_message_flow(self, app_message): """ Handle protocol flow for incoming and outgoing messages, depending on service level and according to MQTT spec. paragraph 4.3-Quality of Service levels and protocol flows :param app_message: PublishMessage to handle :return: nothing. """ if app_message.qos == QOS_0: yield from self._handle_qos0_message_flow(app_message) elif app_message.qos == QOS_1: yield from self._handle_qos1_message_flow(app_message) elif app_message.qos == QOS_2: yield from self._handle_qos2_message_flow(app_message) else: raise HBMQTTException("Unexcepted QOS value '%d" % str(app_message.qos)) @asyncio.coroutine def _handle_qos0_message_flow(self, app_message): """ Handle QOS_0 application message acknowledgment For incoming messages, this method stores the message For outgoing messages, this methods sends PUBLISH :param app_message: :return: """ assert app_message.qos == QOS_0 if app_message.direction == OUTGOING: packet = app_message.build_publish_packet() # Send PUBLISH packet yield from self._send_packet(packet) app_message.publish_packet = packet elif app_message.direction == INCOMING: if app_message.publish_packet.dup_flag: self.logger.warning("[MQTT-3.3.1-2] DUP flag must set to 0 for QOS 0 message. Message ignored: %s" % repr(app_message.publish_packet)) else: try: self.session.delivered_message_queue.put_nowait(app_message) except: self.logger.warning("delivered messages queue full. QOS_0 message discarded") @asyncio.coroutine def _handle_qos1_message_flow(self, app_message): """ Handle QOS_1 application message acknowledgment For incoming messages, this method stores the message and reply with PUBACK For outgoing messages, this methods sends PUBLISH and waits for the corresponding PUBACK :param app_message: :return: """ assert app_message.qos == QOS_1 if app_message.puback_packet: raise HBMQTTException("Message '%d' has already been acknowledged" % app_message.packet_id) if app_message.direction == OUTGOING: if app_message.packet_id not in self.session.inflight_out: # Store message in session self.session.inflight_out[app_message.packet_id] = app_message if app_message.publish_packet is not None: # A Publish packet has already been sent, this is a retry publish_packet = app_message.build_publish_packet(dup=True) else: publish_packet = app_message.build_publish_packet() # Send PUBLISH packet yield from self._send_packet(publish_packet) app_message.publish_packet = publish_packet # Wait for puback waiter = asyncio.Future(loop=self._loop) self._puback_waiters[app_message.packet_id] = waiter yield from waiter del self._puback_waiters[app_message.packet_id] app_message.puback_packet = waiter.result() # Discard inflight message del self.session.inflight_out[app_message.packet_id] elif app_message.direction == INCOMING: # Initiate delivery self.logger.debug("Add message to delivery") yield from self.session.delivered_message_queue.put(app_message) # Send PUBACK puback = PubackPacket.build(app_message.packet_id) yield from self._send_packet(puback) app_message.puback_packet = puback @asyncio.coroutine def _handle_qos2_message_flow(self, app_message): """ Handle QOS_2 application message acknowledgment For incoming messages, this method stores the message, sends PUBREC, waits for PUBREL, initiate delivery and send PUBCOMP For outgoing messages, this methods sends PUBLISH, waits for PUBREC, discards messages and wait for PUBCOMP :param app_message: :return: """ assert app_message.qos == QOS_2 if app_message.direction == OUTGOING: if app_message.pubrel_packet and app_message.pubcomp_packet: raise HBMQTTException("Message '%d' has already been acknowledged" % app_message.packet_id) if not app_message.pubrel_packet: # Store message if app_message.publish_packet is not None: # This is a retry flow, no need to store just check the message exists in session if app_message.packet_id not in self.session.inflight_out: raise HBMQTTException("Unknown inflight message '%d' in session" % app_message.packet_id) publish_packet = app_message.build_publish_packet(dup=True) else: # Store message in session self.session.inflight_out[app_message.packet_id] = app_message publish_packet = app_message.build_publish_packet() # Send PUBLISH packet yield from self._send_packet(publish_packet) app_message.publish_packet = publish_packet # Wait PUBREC if app_message.packet_id in self._pubrec_waiters: # PUBREC waiter already exists for this packet ID message = "Can't add PUBREC waiter, a waiter already exists for message Id '%s'" \ % app_message.packet_id self.logger.warning(message) raise HBMQTTException(message) waiter = asyncio.Future(loop=self._loop) self._pubrec_waiters[app_message.packet_id] = waiter yield from waiter del self._pubrec_waiters[app_message.packet_id] app_message.pubrec_packet = waiter.result() if not app_message.pubcomp_packet: # Send pubrel app_message.pubrel_packet = PubrelPacket.build(app_message.packet_id) yield from self._send_packet(app_message.pubrel_packet) # Wait for PUBCOMP waiter = asyncio.Future(loop=self._loop) self._pubcomp_waiters[app_message.packet_id] = waiter yield from waiter del self._pubcomp_waiters[app_message.packet_id] app_message.pubcomp_packet = waiter.result() # Discard inflight message del self.session.inflight_out[app_message.packet_id] elif app_message.direction == INCOMING: self.session.inflight_in[app_message.packet_id] = app_message # Send pubrec pubrec_packet = PubrecPacket.build(app_message.packet_id) yield from self._send_packet(pubrec_packet) app_message.pubrec_packet = pubrec_packet # Wait PUBREL if app_message.packet_id in self._pubrel_waiters and not self._pubrel_waiters[app_message.packet_id].done(): # PUBREL waiter already exists for this packet ID message = "A waiter already exists for message Id '%s', canceling it" \ % app_message.packet_id self.logger.warning(message) self._pubrel_waiters[app_message.packet_id].cancel() try: waiter = asyncio.Future(loop=self._loop) self._pubrel_waiters[app_message.packet_id] = waiter yield from waiter del self._pubrel_waiters[app_message.packet_id] app_message.pubrel_packet = waiter.result() # Initiate delivery and discard message yield from self.session.delivered_message_queue.put(app_message) del self.session.inflight_in[app_message.packet_id] # Send pubcomp pubcomp_packet = PubcompPacket.build(app_message.packet_id) yield from self._send_packet(pubcomp_packet) app_message.pubcomp_packet = pubcomp_packet except asyncio.CancelledError: self.logger.debug("Message flow cancelled") @asyncio.coroutine def _reader_loop(self): self.logger.debug("%s Starting reader coro" % self.session.client_id) running_tasks = collections.deque() keepalive_timeout = self.session.keep_alive if keepalive_timeout <= 0: keepalive_timeout = None while True: try: self._reader_ready.set() while running_tasks and running_tasks[0].done(): running_tasks.popleft() if len(running_tasks) > 1: self.logger.debug("handler running tasks: %d" % len(running_tasks)) fixed_header = yield from asyncio.wait_for( MQTTFixedHeader.from_stream(self.reader), keepalive_timeout, loop=self._loop) if fixed_header: if fixed_header.packet_type == RESERVED_0 or fixed_header.packet_type == RESERVED_15: self.logger.warning("%s Received reserved packet, which is forbidden: closing connection" % (self.session.client_id)) yield from self.handle_connection_closed() else: cls = packet_class(fixed_header) packet = yield from cls.from_stream(self.reader, fixed_header=fixed_header) yield from self.plugins_manager.fire_event( EVENT_MQTT_PACKET_RECEIVED, packet=packet, session=self.session) task = None if packet.fixed_header.packet_type == CONNACK: task = asyncio.ensure_future(self.handle_connack(packet), loop=self._loop) elif packet.fixed_header.packet_type == SUBSCRIBE: task = asyncio.ensure_future(self.handle_subscribe(packet), loop=self._loop) elif packet.fixed_header.packet_type == UNSUBSCRIBE: task = asyncio.ensure_future(self.handle_unsubscribe(packet), loop=self._loop) elif packet.fixed_header.packet_type == SUBACK: task = asyncio.ensure_future(self.handle_suback(packet), loop=self._loop) elif packet.fixed_header.packet_type == UNSUBACK: task = asyncio.ensure_future(self.handle_unsuback(packet), loop=self._loop) elif packet.fixed_header.packet_type == PUBACK: task = asyncio.ensure_future(self.handle_puback(packet), loop=self._loop) elif packet.fixed_header.packet_type == PUBREC: task = asyncio.ensure_future(self.handle_pubrec(packet), loop=self._loop) elif packet.fixed_header.packet_type == PUBREL: task = asyncio.ensure_future(self.handle_pubrel(packet), loop=self._loop) elif packet.fixed_header.packet_type == PUBCOMP: task = asyncio.ensure_future(self.handle_pubcomp(packet), loop=self._loop) elif packet.fixed_header.packet_type == PINGREQ: task = asyncio.ensure_future(self.handle_pingreq(packet), loop=self._loop) elif packet.fixed_header.packet_type == PINGRESP: task = asyncio.ensure_future(self.handle_pingresp(packet), loop=self._loop) elif packet.fixed_header.packet_type == PUBLISH: task = asyncio.ensure_future(self.handle_publish(packet), loop=self._loop) elif packet.fixed_header.packet_type == DISCONNECT: task = asyncio.ensure_future(self.handle_disconnect(packet), loop=self._loop) elif packet.fixed_header.packet_type == CONNECT: self.handle_connect(packet) else: self.logger.warning("%s Unhandled packet type: %s" % (self.session.client_id, packet.fixed_header.packet_type)) if task: running_tasks.append(task) else: self.logger.debug("%s No more data (EOF received), stopping reader coro" % self.session.client_id) break except MQTTException: self.logger.debug("Message discarded") except asyncio.CancelledError: self.logger.debug("Task cancelled, reader loop ending") break except asyncio.TimeoutError: self.logger.debug("%s Input stream read timeout" % self.session.client_id) self.handle_read_timeout() except NoDataException: self.logger.debug("%s No data available" % self.session.client_id) except BaseException as e: self.logger.warning("%s Unhandled exception in reader coro: %r" % (type(self).__name__, e)) break while running_tasks: running_tasks.popleft().cancel() yield from self.handle_connection_closed() self._reader_stopped.set() self.logger.debug("%s Reader coro stopped" % self.session.client_id) yield from self.stop() @asyncio.coroutine def _send_packet(self, packet): try: with (yield from self._write_lock): yield from packet.to_stream(self.writer) if self._keepalive_task: self._keepalive_task.cancel() self._keepalive_task = self._loop.call_later(self.keepalive_timeout, self.handle_write_timeout) yield from self.plugins_manager.fire_event(EVENT_MQTT_PACKET_SENT, packet=packet, session=self.session) except ConnectionResetError as cre: yield from self.handle_connection_closed() raise except BaseException as e: self.logger.warning("Unhandled exception: %s" % e) raise @asyncio.coroutine def mqtt_deliver_next_message(self): if self.logger.isEnabledFor(logging.DEBUG): self.logger.debug("%d message(s) available for delivery" % self.session.delivered_message_queue.qsize()) try: message = yield from self.session.delivered_message_queue.get() except asyncio.CancelledError: message = None if self.logger.isEnabledFor(logging.DEBUG): self.logger.debug("Delivering message %s" % message) return message def handle_write_timeout(self): self.logger.debug('%s write timeout unhandled' % self.session.client_id) def handle_read_timeout(self): self.logger.debug('%s read timeout unhandled' % self.session.client_id) @asyncio.coroutine def handle_connack(self, connack: ConnackPacket): self.logger.debug('%s CONNACK unhandled' % self.session.client_id) @asyncio.coroutine def handle_connect(self, connect: ConnectPacket): self.logger.debug('%s CONNECT unhandled' % self.session.client_id) @asyncio.coroutine def handle_subscribe(self, subscribe: SubscribePacket): self.logger.debug('%s SUBSCRIBE unhandled' % self.session.client_id) @asyncio.coroutine def handle_unsubscribe(self, subscribe: UnsubscribePacket): self.logger.debug('%s UNSUBSCRIBE unhandled' % self.session.client_id) @asyncio.coroutine def handle_suback(self, suback: SubackPacket): self.logger.debug('%s SUBACK unhandled' % self.session.client_id) @asyncio.coroutine def handle_unsuback(self, unsuback: UnsubackPacket): self.logger.debug('%s UNSUBACK unhandled' % self.session.client_id) @asyncio.coroutine def handle_pingresp(self, pingresp: PingRespPacket): self.logger.debug('%s PINGRESP unhandled' % self.session.client_id) @asyncio.coroutine def handle_pingreq(self, pingreq: PingReqPacket): self.logger.debug('%s PINGREQ unhandled' % self.session.client_id) @asyncio.coroutine def handle_disconnect(self, disconnect: DisconnectPacket): self.logger.debug('%s DISCONNECT unhandled' % self.session.client_id) @asyncio.coroutine def handle_connection_closed(self): self.logger.debug('%s Connection closed unhandled' % self.session.client_id) @asyncio.coroutine def handle_puback(self, puback: PubackPacket): packet_id = puback.variable_header.packet_id try: waiter = self._puback_waiters[packet_id] waiter.set_result(puback) except KeyError: self.logger.warning("Received PUBACK for unknown pending message Id: '%d'" % packet_id) except InvalidStateError: self.logger.warning("PUBACK waiter with Id '%d' already done" % packet_id) @asyncio.coroutine def handle_pubrec(self, pubrec: PubrecPacket): packet_id = pubrec.packet_id try: waiter = self._pubrec_waiters[packet_id] waiter.set_result(pubrec) except KeyError: self.logger.warning("Received PUBREC for unknown pending message with Id: %d" % packet_id) except InvalidStateError: self.logger.warning("PUBREC waiter with Id '%d' already done" % packet_id) @asyncio.coroutine def handle_pubcomp(self, pubcomp: PubcompPacket): packet_id = pubcomp.packet_id try: waiter = self._pubcomp_waiters[packet_id] waiter.set_result(pubcomp) except KeyError: self.logger.warning("Received PUBCOMP for unknown pending message with Id: %d" % packet_id) except InvalidStateError: self.logger.warning("PUBCOMP waiter with Id '%d' already done" % packet_id) @asyncio.coroutine def handle_pubrel(self, pubrel: PubrelPacket): packet_id = pubrel.packet_id try: waiter = self._pubrel_waiters[packet_id] waiter.set_result(pubrel) except KeyError: self.logger.warning("Received PUBREL for unknown pending message with Id: %d" % packet_id) except InvalidStateError: self.logger.warning("PUBREL waiter with Id '%d' already done" % packet_id) @asyncio.coroutine def handle_publish(self, publish_packet: PublishPacket): packet_id = publish_packet.variable_header.packet_id qos = publish_packet.qos incoming_message = IncomingApplicationMessage(packet_id, publish_packet.topic_name, qos, publish_packet.data, publish_packet.retain_flag) incoming_message.publish_packet = publish_packet yield from self._handle_message_flow(incoming_message) self.logger.debug("Message queue size: %d" % self.session.delivered_message_queue.qsize()) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1579961068.0 hbmqtt-0.9.6/hbmqtt/mqtt/puback.py0000644000175000017510000000223500000000000017036 0ustar00niconico00000000000000# Copyright (c) 2015 Nicolas JOUANIN # # See the file license.txt for copying permission. from hbmqtt.mqtt.packet import MQTTPacket, MQTTFixedHeader, PUBACK, PacketIdVariableHeader from hbmqtt.errors import HBMQTTException class PubackPacket(MQTTPacket): VARIABLE_HEADER = PacketIdVariableHeader PAYLOAD = None @property def packet_id(self): return self.variable_header.packet_id @packet_id.setter def packet_id(self, val: int): self.variable_header.packet_id = val def __init__(self, fixed: MQTTFixedHeader=None, variable_header: PacketIdVariableHeader=None): if fixed is None: header = MQTTFixedHeader(PUBACK, 0x00) else: if fixed.packet_type is not PUBACK: raise HBMQTTException("Invalid fixed packet type %s for PubackPacket init" % fixed.packet_type) header = fixed super().__init__(header) self.variable_header = variable_header self.payload = None @classmethod def build(cls, packet_id: int): v_header = PacketIdVariableHeader(packet_id) packet = PubackPacket(variable_header=v_header) return packet ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1579961068.0 hbmqtt-0.9.6/hbmqtt/mqtt/pubcomp.py0000644000175000017510000000224300000000000017235 0ustar00niconico00000000000000# Copyright (c) 2015 Nicolas JOUANIN # # See the file license.txt for copying permission. from hbmqtt.mqtt.packet import MQTTPacket, MQTTFixedHeader, PUBCOMP, PacketIdVariableHeader from hbmqtt.errors import HBMQTTException class PubcompPacket(MQTTPacket): VARIABLE_HEADER = PacketIdVariableHeader PAYLOAD = None @property def packet_id(self): return self.variable_header.packet_id @packet_id.setter def packet_id(self, val: int): self.variable_header.packet_id = val def __init__(self, fixed: MQTTFixedHeader=None, variable_header: PacketIdVariableHeader=None): if fixed is None: header = MQTTFixedHeader(PUBCOMP, 0x00) else: if fixed.packet_type is not PUBCOMP: raise HBMQTTException("Invalid fixed packet type %s for PubcompPacket init" % fixed.packet_type) header = fixed super().__init__(header) self.variable_header = variable_header self.payload = None @classmethod def build(cls, packet_id: int): v_header = PacketIdVariableHeader(packet_id) packet = PubcompPacket(variable_header=v_header) return packet ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1579961068.0 hbmqtt-0.9.6/hbmqtt/mqtt/publish.py0000644000175000017510000001204400000000000017236 0ustar00niconico00000000000000# Copyright (c) 2015 Nicolas JOUANIN # # See the file license.txt for copying permission. import asyncio from hbmqtt.mqtt.packet import MQTTPacket, MQTTFixedHeader, PUBLISH, MQTTVariableHeader, MQTTPayload from hbmqtt.errors import HBMQTTException, MQTTException from hbmqtt.codecs import decode_packet_id, decode_string, encode_string, int_to_bytes class PublishVariableHeader(MQTTVariableHeader): __slots__ = ('topic_name', 'packet_id') def __init__(self, topic_name: str, packet_id: int=None): super().__init__() if '*' in topic_name: raise MQTTException("[MQTT-3.3.2-2] Topic name in the PUBLISH Packet MUST NOT contain wildcard characters.") self.topic_name = topic_name self.packet_id = packet_id def __repr__(self): return type(self).__name__ + '(topic={0}, packet_id={1})'.format(self.topic_name, self.packet_id) def to_bytes(self): out = bytearray() out.extend(encode_string(self.topic_name)) if self.packet_id is not None: out.extend(int_to_bytes(self.packet_id, 2)) return out @classmethod @asyncio.coroutine def from_stream(cls, reader: asyncio.StreamReader, fixed_header: MQTTFixedHeader): topic_name = yield from decode_string(reader) has_qos = (fixed_header.flags >> 1) & 0x03 if has_qos: packet_id = yield from decode_packet_id(reader) else: packet_id = None return cls(topic_name, packet_id) class PublishPayload(MQTTPayload): __slots__ = ('data',) def __init__(self, data: bytes=None): super().__init__() self.data = data def to_bytes(self, fixed_header: MQTTFixedHeader, variable_header: MQTTVariableHeader): return self.data @classmethod @asyncio.coroutine def from_stream(cls, reader: asyncio.StreamReader, fixed_header: MQTTFixedHeader, variable_header: MQTTVariableHeader): data = bytearray() data_length = fixed_header.remaining_length - variable_header.bytes_length length_read = 0 while length_read < data_length: buffer = yield from reader.read(data_length - length_read) data.extend(buffer) length_read = len(data) return cls(data) def __repr__(self): return type(self).__name__ + '(data={0!r})'.format(repr(self.data)) class PublishPacket(MQTTPacket): VARIABLE_HEADER = PublishVariableHeader PAYLOAD = PublishPayload DUP_FLAG = 0x08 RETAIN_FLAG = 0x01 QOS_FLAG = 0x06 def __init__(self, fixed: MQTTFixedHeader=None, variable_header: PublishVariableHeader=None, payload=None): if fixed is None: header = MQTTFixedHeader(PUBLISH, 0x00) else: if fixed.packet_type is not PUBLISH: raise HBMQTTException("Invalid fixed packet type %s for PublishPacket init" % fixed.packet_type) header = fixed super().__init__(header) self.variable_header = variable_header self.payload = payload def set_flags(self, dup_flag=False, qos=0, retain_flag=False): self.dup_flag = dup_flag self.retain_flag = retain_flag self.qos = qos def _set_header_flag(self, val, mask): if val: self.fixed_header.flags |= mask else: self.fixed_header.flags &= ~mask def _get_header_flag(self, mask): if self.fixed_header.flags & mask: return True else: return False @property def dup_flag(self) -> bool: return self._get_header_flag(self.DUP_FLAG) @dup_flag.setter def dup_flag(self, val: bool): self._set_header_flag(val, self.DUP_FLAG) @property def retain_flag(self) -> bool: return self._get_header_flag(self.RETAIN_FLAG) @retain_flag.setter def retain_flag(self, val: bool): self._set_header_flag(val, self.RETAIN_FLAG) @property def qos(self): return (self.fixed_header.flags & self.QOS_FLAG) >> 1 @qos.setter def qos(self, val: int): self.fixed_header.flags &= 0xf9 self.fixed_header.flags |= (val << 1) @property def packet_id(self): return self.variable_header.packet_id @packet_id.setter def packet_id(self, val: int): self.variable_header.packet_id = val @property def data(self): return self.payload.data @data.setter def data(self, data: bytes): self.payload.data = data @property def topic_name(self): return self.variable_header.topic_name @topic_name.setter def topic_name(self, name: str): self.variable_header.topic_name = name @classmethod def build(cls, topic_name: str, message: bytes, packet_id: int, dup_flag, qos, retain): v_header = PublishVariableHeader(topic_name, packet_id) payload = PublishPayload(message) packet = PublishPacket(variable_header=v_header, payload=payload) packet.dup_flag = dup_flag packet.retain_flag = retain packet.qos = qos return packet ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1579961068.0 hbmqtt-0.9.6/hbmqtt/mqtt/pubrec.py0000644000175000017510000000223500000000000017051 0ustar00niconico00000000000000# Copyright (c) 2015 Nicolas JOUANIN # # See the file license.txt for copying permission. from hbmqtt.mqtt.packet import MQTTPacket, MQTTFixedHeader, PUBREC, PacketIdVariableHeader from hbmqtt.errors import HBMQTTException class PubrecPacket(MQTTPacket): VARIABLE_HEADER = PacketIdVariableHeader PAYLOAD = None @property def packet_id(self): return self.variable_header.packet_id @packet_id.setter def packet_id(self, val: int): self.variable_header.packet_id = val def __init__(self, fixed: MQTTFixedHeader=None, variable_header: PacketIdVariableHeader=None): if fixed is None: header = MQTTFixedHeader(PUBREC, 0x00) else: if fixed.packet_type is not PUBREC: raise HBMQTTException("Invalid fixed packet type %s for PubrecPacket init" % fixed.packet_type) header = fixed super().__init__(header) self.variable_header = variable_header self.payload = None @classmethod def build(cls, packet_id: int): v_header = PacketIdVariableHeader(packet_id) packet = PubrecPacket(variable_header=v_header) return packet ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1579961068.0 hbmqtt-0.9.6/hbmqtt/mqtt/pubrel.py0000644000175000017510000000224000000000000017056 0ustar00niconico00000000000000# Copyright (c) 2015 Nicolas JOUANIN # # See the file license.txt for copying permission. from hbmqtt.mqtt.packet import MQTTPacket, MQTTFixedHeader, PUBREL, PacketIdVariableHeader from hbmqtt.errors import HBMQTTException class PubrelPacket(MQTTPacket): VARIABLE_HEADER = PacketIdVariableHeader PAYLOAD = None @property def packet_id(self): return self.variable_header.packet_id @packet_id.setter def packet_id(self, val: int): self.variable_header.packet_id = val def __init__(self, fixed: MQTTFixedHeader=None, variable_header: PacketIdVariableHeader=None): if fixed is None: header = MQTTFixedHeader(PUBREL, 0x02) # [MQTT-3.6.1-1] else: if fixed.packet_type is not PUBREL: raise HBMQTTException("Invalid fixed packet type %s for PubrelPacket init" % fixed.packet_type) header = fixed super().__init__(header) self.variable_header = variable_header self.payload = None @classmethod def build(cls, packet_id): variable_header = PacketIdVariableHeader(packet_id) return PubrelPacket(variable_header=variable_header) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1579961068.0 hbmqtt-0.9.6/hbmqtt/mqtt/suback.py0000644000175000017510000000467000000000000017046 0ustar00niconico00000000000000# Copyright (c) 2015 Nicolas JOUANIN # # See the file license.txt for copying permission. import asyncio from hbmqtt.mqtt.packet import MQTTPacket, MQTTFixedHeader, SUBACK, PacketIdVariableHeader, MQTTPayload, MQTTVariableHeader from hbmqtt.errors import HBMQTTException, NoDataException from hbmqtt.adapters import ReaderAdapter from hbmqtt.codecs import bytes_to_int, int_to_bytes, read_or_raise class SubackPayload(MQTTPayload): __slots__ = ('return_codes',) RETURN_CODE_00 = 0x00 RETURN_CODE_01 = 0x01 RETURN_CODE_02 = 0x02 RETURN_CODE_80 = 0x80 def __init__(self, return_codes=[]): super().__init__() self.return_codes = return_codes def __repr__(self): return type(self).__name__ + '(return_codes={0})'.format(repr(self.return_codes)) def to_bytes(self, fixed_header: MQTTFixedHeader, variable_header: MQTTVariableHeader): out = b'' for return_code in self.return_codes: out += int_to_bytes(return_code, 1) return out @classmethod @asyncio.coroutine def from_stream(cls, reader: ReaderAdapter, fixed_header: MQTTFixedHeader, variable_header: MQTTVariableHeader): return_codes = [] bytes_to_read = fixed_header.remaining_length - variable_header.bytes_length for i in range(0, bytes_to_read): try: return_code_byte = yield from read_or_raise(reader, 1) return_code = bytes_to_int(return_code_byte) return_codes.append(return_code) except NoDataException: break return cls(return_codes) class SubackPacket(MQTTPacket): VARIABLE_HEADER = PacketIdVariableHeader PAYLOAD = SubackPayload def __init__(self, fixed: MQTTFixedHeader=None, variable_header: PacketIdVariableHeader=None, payload=None): if fixed is None: header = MQTTFixedHeader(SUBACK, 0x00) else: if fixed.packet_type is not SUBACK: raise HBMQTTException("Invalid fixed packet type %s for SubackPacket init" % fixed.packet_type) header = fixed super().__init__(header) self.variable_header = variable_header self.payload = payload @classmethod def build(cls, packet_id, return_codes): variable_header = cls.VARIABLE_HEADER(packet_id) payload = cls.PAYLOAD(return_codes) return cls(variable_header=variable_header, payload=payload) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1579961068.0 hbmqtt-0.9.6/hbmqtt/mqtt/subscribe.py0000644000175000017510000000472000000000000017553 0ustar00niconico00000000000000# Copyright (c) 2015 Nicolas JOUANIN # # See the file license.txt for copying permission. import asyncio from hbmqtt.mqtt.packet import MQTTPacket, MQTTFixedHeader, SUBSCRIBE, PacketIdVariableHeader, MQTTPayload, MQTTVariableHeader from hbmqtt.errors import HBMQTTException, NoDataException from hbmqtt.codecs import bytes_to_int, decode_string, encode_string, int_to_bytes, read_or_raise class SubscribePayload(MQTTPayload): __slots__ = ('topics',) def __init__(self, topics=[]): super().__init__() self.topics = topics def to_bytes(self, fixed_header: MQTTFixedHeader, variable_header: MQTTVariableHeader): out = b'' for topic in self.topics: out += encode_string(topic[0]) out += int_to_bytes(topic[1], 1) return out @classmethod @asyncio.coroutine def from_stream(cls, reader: asyncio.StreamReader, fixed_header: MQTTFixedHeader, variable_header: MQTTVariableHeader): topics = [] payload_length = fixed_header.remaining_length - variable_header.bytes_length read_bytes = 0 while read_bytes < payload_length: try: topic = yield from decode_string(reader) qos_byte = yield from read_or_raise(reader, 1) qos = bytes_to_int(qos_byte) topics.append((topic, qos)) read_bytes += 2 + len(topic.encode('utf-8')) + 1 except NoDataException as exc: break return cls(topics) def __repr__(self): return type(self).__name__ + '(topics={0!r})'.format(self.topics) class SubscribePacket(MQTTPacket): VARIABLE_HEADER = PacketIdVariableHeader PAYLOAD = SubscribePayload def __init__(self, fixed: MQTTFixedHeader=None, variable_header: PacketIdVariableHeader=None, payload=None): if fixed is None: header = MQTTFixedHeader(SUBSCRIBE, 0x02) # [MQTT-3.8.1-1] else: if fixed.packet_type is not SUBSCRIBE: raise HBMQTTException("Invalid fixed packet type %s for SubscribePacket init" % fixed.packet_type) header = fixed super().__init__(header) self.variable_header = variable_header self.payload = payload @classmethod def build(cls, topics, packet_id): v_header = PacketIdVariableHeader(packet_id) payload = SubscribePayload(topics) return SubscribePacket(variable_header=v_header, payload=payload) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1579961068.0 hbmqtt-0.9.6/hbmqtt/mqtt/unsuback.py0000644000175000017510000000174400000000000017410 0ustar00niconico00000000000000# Copyright (c) 2015 Nicolas JOUANIN # # See the file license.txt for copying permission. from hbmqtt.mqtt.packet import MQTTPacket, MQTTFixedHeader, UNSUBACK, PacketIdVariableHeader from hbmqtt.errors import HBMQTTException class UnsubackPacket(MQTTPacket): VARIABLE_HEADER = PacketIdVariableHeader PAYLOAD = None def __init__(self, fixed: MQTTFixedHeader=None, variable_header: PacketIdVariableHeader=None, payload=None): if fixed is None: header = MQTTFixedHeader(UNSUBACK, 0x00) else: if fixed.packet_type is not UNSUBACK: raise HBMQTTException("Invalid fixed packet type %s for UnsubackPacket init" % fixed.packet_type) header = fixed super().__init__(header) self.variable_header = variable_header self.payload = payload @classmethod def build(cls, packet_id): variable_header = PacketIdVariableHeader(packet_id) return cls(variable_header=variable_header) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1579961068.0 hbmqtt-0.9.6/hbmqtt/mqtt/unsubscribe.py0000644000175000017510000000424400000000000020117 0ustar00niconico00000000000000# Copyright (c) 2015 Nicolas JOUANIN # # See the file license.txt for copying permission. import asyncio from hbmqtt.mqtt.packet import MQTTPacket, MQTTFixedHeader, UNSUBSCRIBE, PacketIdVariableHeader, MQTTPayload, MQTTVariableHeader from hbmqtt.errors import HBMQTTException, NoDataException from hbmqtt.codecs import decode_string, encode_string class UnubscribePayload(MQTTPayload): __slots__ = ('topics',) def __init__(self, topics=[]): super().__init__() self.topics = topics def to_bytes(self, fixed_header: MQTTFixedHeader, variable_header: MQTTVariableHeader): out = b'' for topic in self.topics: out += encode_string(topic) return out @classmethod @asyncio.coroutine def from_stream(cls, reader: asyncio.StreamReader, fixed_header: MQTTFixedHeader, variable_header: MQTTVariableHeader): topics = [] payload_length = fixed_header.remaining_length - variable_header.bytes_length read_bytes = 0 while read_bytes < payload_length: try: topic = yield from decode_string(reader) topics.append(topic) read_bytes += 2 + len(topic.encode('utf-8')) except NoDataException: break return cls(topics) class UnsubscribePacket(MQTTPacket): VARIABLE_HEADER = PacketIdVariableHeader PAYLOAD = UnubscribePayload def __init__(self, fixed: MQTTFixedHeader=None, variable_header: PacketIdVariableHeader=None, payload=None): if fixed is None: header = MQTTFixedHeader(UNSUBSCRIBE, 0x02) # [MQTT-3.10.1-1] else: if fixed.packet_type is not UNSUBSCRIBE: raise HBMQTTException("Invalid fixed packet type %s for UnsubscribePacket init" % fixed.packet_type) header = fixed super().__init__(header) self.variable_header = variable_header self.payload = payload @classmethod def build(cls, topics, packet_id): v_header = PacketIdVariableHeader(packet_id) payload = UnubscribePayload(topics) return UnsubscribePacket(variable_header=v_header, payload=payload) ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1579961563.6934717 hbmqtt-0.9.6/hbmqtt/plugins/0000755000175000017510000000000000000000000015711 5ustar00niconico00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1579961068.0 hbmqtt-0.9.6/hbmqtt/plugins/__init__.py0000644000175000017510000000013200000000000020016 0ustar00niconico00000000000000# Copyright (c) 2015 Nicolas JOUANIN # # See the file license.txt for copying permission. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1579961068.0 hbmqtt-0.9.6/hbmqtt/plugins/authentication.py0000644000175000017510000000744600000000000021315 0ustar00niconico00000000000000# Copyright (c) 2015 Nicolas JOUANIN # # See the file license.txt for copying permission. import logging import asyncio from passlib.apps import custom_app_context as pwd_context class BaseAuthPlugin: def __init__(self, context): self.context = context try: self.auth_config = self.context.config['auth'] except KeyError: self.context.logger.warning("'auth' section not found in context configuration") def authenticate(self, *args, **kwargs): if not self.auth_config: # auth config section not found self.context.logger.warning("'auth' section not found in context configuration") return False return True class AnonymousAuthPlugin(BaseAuthPlugin): def __init__(self, context): super().__init__(context) @asyncio.coroutine def authenticate(self, *args, **kwargs): authenticated = super().authenticate(*args, **kwargs) if authenticated: allow_anonymous = self.auth_config.get('allow-anonymous', True) # allow anonymous by default if allow_anonymous: authenticated = True self.context.logger.debug("Authentication success: config allows anonymous") else: try: session = kwargs.get('session', None) authenticated = True if session.username else False if self.context.logger.isEnabledFor(logging.DEBUG): if authenticated: self.context.logger.debug("Authentication success: session has a non empty username") else: self.context.logger.debug("Authentication failure: session has an empty username") except KeyError: self.context.logger.warning("Session informations not available") authenticated = False return authenticated class FileAuthPlugin(BaseAuthPlugin): def __init__(self, context): super().__init__(context) self._users = dict() self._read_password_file() def _read_password_file(self): password_file = self.auth_config.get('password-file', None) if password_file: try: with open(password_file) as f: self.context.logger.debug("Reading user database from %s" % password_file) for l in f: line = l.strip() if not line.startswith('#'): # Allow comments in files (username, pwd_hash) = line.split(sep=":", maxsplit=3) if username: self._users[username] = pwd_hash self.context.logger.debug("user %s , hash=%s" % (username, pwd_hash)) self.context.logger.debug("%d user(s) read from file %s" % (len(self._users), password_file)) except FileNotFoundError: self.context.logger.warning("Password file %s not found" % password_file) else: self.context.logger.debug("Configuration parameter 'password_file' not found") @asyncio.coroutine def authenticate(self, *args, **kwargs): authenticated = super().authenticate(*args, **kwargs) if authenticated: session = kwargs.get('session', None) if session.username: hash = self._users.get(session.username, None) if not hash: authenticated = False self.context.logger.debug("No hash found for user '%s'" % session.username) else: authenticated = pwd_context.verify(session.password, hash) else: return None return authenticated ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1579961068.0 hbmqtt-0.9.6/hbmqtt/plugins/logging.py0000644000175000017510000000272100000000000017713 0ustar00niconico00000000000000# Copyright (c) 2015 Nicolas JOUANIN # # See the file license.txt for copying permission. import logging import asyncio from functools import partial class EventLoggerPlugin: def __init__(self, context): self.context = context @asyncio.coroutine def log_event(self, *args, **kwargs): self.context.logger.info("### '%s' EVENT FIRED ###" % kwargs['event_name'].replace('old', '')) def __getattr__(self, name): if name.startswith("on_"): return partial(self.log_event, event_name=name) class PacketLoggerPlugin: def __init__(self, context): self.context = context @asyncio.coroutine def on_mqtt_packet_received(self, *args, **kwargs): packet = kwargs.get('packet') session = kwargs.get('session', None) if self.context.logger.isEnabledFor(logging.DEBUG): if session: self.context.logger.debug("%s <-in-- %s" % (session.client_id, repr(packet))) else: self.context.logger.debug("<-in-- %s" % repr(packet)) @asyncio.coroutine def on_mqtt_packet_sent(self, *args, **kwargs): packet = kwargs.get('packet') session = kwargs.get('session', None) if self.context.logger.isEnabledFor(logging.DEBUG): if session: self.context.logger.debug("%s -out-> %s" % (session.client_id, repr(packet))) else: self.context.logger.debug("-out-> %s" % repr(packet)) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1579961068.0 hbmqtt-0.9.6/hbmqtt/plugins/manager.py0000644000175000017510000001610700000000000017702 0ustar00niconico00000000000000# Copyright (c) 2015 Nicolas JOUANIN # # See the file license.txt for copying permission. __all__ = ['get_plugin_manager', 'BaseContext', 'PluginManager'] import pkg_resources import logging import asyncio import copy import sys from collections import namedtuple Plugin = namedtuple('Plugin', ['name', 'ep', 'object']) plugins_manager = dict() def get_plugin_manager(namespace): global plugins_manager return plugins_manager.get(namespace, None) class BaseContext: def __init__(self): self.loop = None self.logger = None class PluginManager: """ Wraps setuptools Entry point mechanism to provide a basic plugin system. Plugins are loaded for a given namespace (group). This plugin manager uses coroutines to run plugin call asynchronously in an event queue """ def __init__(self, namespace, context, loop=None): global plugins_manager if loop is not None: self._loop = loop else: self._loop = asyncio.get_event_loop() self.logger = logging.getLogger(namespace) if context is None: self.context = BaseContext() else: self.context = context self.context.loop = self._loop self._plugins = [] self._load_plugins(namespace) self._fired_events = [] plugins_manager[namespace] = self @property def app_context(self): return self.context def _load_plugins(self, namespace): self.logger.debug("Loading plugins for namespace %s" % namespace) for ep in pkg_resources.iter_entry_points(group=namespace): plugin = self._load_plugin(ep) self._plugins.append(plugin) self.logger.debug(" Plugin %s ready" % plugin.ep.name) def _load_plugin(self, ep: pkg_resources.EntryPoint): try: self.logger.debug(" Loading plugin %s" % ep) plugin = ep.load(require=True) self.logger.debug(" Initializing plugin %s" % ep) plugin_context = copy.copy(self.app_context) plugin_context.logger = self.logger.getChild(ep.name) obj = plugin(plugin_context) return Plugin(ep.name, ep, obj) except ImportError as ie: self.logger.warning("Plugin %r import failed: %s" % (ep, ie)) except pkg_resources.UnknownExtra as ue: self.logger.warning("Plugin %r dependencies resolution failed: %s" % (ep, ue)) def get_plugin(self, name): """ Get a plugin by its name from the plugins loaded for the current namespace :param name: :return: """ for p in self._plugins: if p.name == name: return p return None @asyncio.coroutine def close(self): """ Free PluginManager resources and cancel pending event methods This method call a close() coroutine for each plugin, allowing plugins to close and free resources :return: """ yield from self.map_plugin_coro("close") for task in self._fired_events: task.cancel() @property def plugins(self): """ Get the loaded plugins list :return: """ return self._plugins def _schedule_coro(self, coro): return asyncio.ensure_future(coro, loop=self._loop) @asyncio.coroutine def fire_event(self, event_name, wait=False, *args, **kwargs): """ Fire an event to plugins. PluginManager schedule @asyncio.coroutinecalls for each plugin on method called "on_" + event_name For example, on_connect will be called on event 'connect' Method calls are schedule in the asyn loop. wait parameter must be set to true to wait until all mehtods are completed. :param event_name: :param args: :param kwargs: :param wait: indicates if fire_event should wait for plugin calls completion (True), or not :return: """ tasks = [] event_method_name = "on_" + event_name for plugin in self._plugins: event_method = getattr(plugin.object, event_method_name, None) if event_method: try: task = self._schedule_coro(event_method(*args, **kwargs)) tasks.append(task) def clean_fired_events(future): try: self._fired_events.remove(task) except (KeyError, ValueError): pass task.add_done_callback(clean_fired_events) except AssertionError: self.logger.error("Method '%s' on plugin '%s' is not a coroutine" % (event_method_name, plugin.name)) self._fired_events.extend(tasks) if wait: if tasks: yield from asyncio.wait(tasks, loop=self._loop) @asyncio.coroutine def map(self, coro, *args, **kwargs): """ Schedule a given coroutine call for each plugin. The coro called get the Plugin instance as first argument of its method call :param coro: coro to call on each plugin :param filter_plugins: list of plugin names to filter (only plugin whose name is in filter are called). None will call all plugins. [] will call None. :param args: arguments to pass to coro :param kwargs: arguments to pass to coro :return: dict containing return from coro call for each plugin """ p_list = kwargs.pop('filter_plugins', None) if p_list is None: p_list = [p.name for p in self.plugins] tasks = [] plugins_list = [] for plugin in self._plugins: if plugin.name in p_list: coro_instance = coro(plugin, *args, **kwargs) if coro_instance: try: tasks.append(self._schedule_coro(coro_instance)) plugins_list.append(plugin) except AssertionError: self.logger.error("Method '%r' on plugin '%s' is not a coroutine" % (coro, plugin.name)) if tasks: ret_list = yield from asyncio.gather(*tasks, loop=self._loop) # Create result map plugin=>ret ret_dict = {k: v for k, v in zip(plugins_list, ret_list)} else: ret_dict = {} return ret_dict @staticmethod @asyncio.coroutine def _call_coro(plugin, coro_name, *args, **kwargs): try: coro = getattr(plugin.object, coro_name, None)(*args, **kwargs) return (yield from coro) except TypeError: # Plugin doesn't implement coro_name return None @asyncio.coroutine def map_plugin_coro(self, coro_name, *args, **kwargs): """ Call a plugin declared by plugin by its name :param coro_name: :param args: :param kwargs: :return: """ return (yield from self.map(self._call_coro, coro_name, *args, **kwargs)) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1579961068.0 hbmqtt-0.9.6/hbmqtt/plugins/persistence.py0000644000175000017510000000453500000000000020616 0ustar00niconico00000000000000# Copyright (c) 2015 Nicolas JOUANIN # # See the file license.txt for copying permission. import asyncio import sqlite3 import pickle class SQLitePlugin: def __init__(self, context): self.context = context self.conn = None self.cursor = None self.db_file = None try: self.persistence_config = self.context.config['persistence'] self.init_db() except KeyError: self.context.logger.warning("'persistence' section not found in context configuration") def init_db(self): self.db_file = self.persistence_config.get('file', None) if not self.db_file: self.context.logger.warning("'file' persistence parameter not found") else: try: self.conn = sqlite3.connect(self.db_file) self.cursor = self.conn.cursor() self.context.logger.info("Database file '%s' opened" % self.db_file) except Exception as e: self.context.logger.error("Error while initializing database '%s' : %s" % (self.db_file, e)) if self.cursor: self.cursor.execute("CREATE TABLE IF NOT EXISTS session(client_id TEXT PRIMARY KEY, data BLOB)") @asyncio.coroutine def save_session(self, session): if self.cursor: dump = pickle.dumps(session) try: self.cursor.execute( "INSERT OR REPLACE INTO session (client_id, data) VALUES (?,?)", (session.client_id, dump)) self.conn.commit() except Exception as e: self.context.logger.error("Failed saving session '%s': %s" % (session, e)) @asyncio.coroutine def find_session(self, client_id): if self.cursor: row = self.cursor.execute("SELECT data FROM session where client_id=?", (client_id,)).fetchone() if row: return pickle.loads(row[0]) else: return None @asyncio.coroutine def del_session(self, client_id): if self.cursor: self.cursor.execute("DELETE FROM session where client_id=?", (client_id,)) self.conn.commit() @asyncio.coroutine def on_broker_post_shutdown(self): if self.conn: self.conn.close() self.context.logger.info("Database file '%s' closed" % self.db_file) ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1579961563.6934717 hbmqtt-0.9.6/hbmqtt/plugins/sys/0000755000175000017510000000000000000000000016527 5ustar00niconico00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1579961068.0 hbmqtt-0.9.6/hbmqtt/plugins/sys/__init__.py0000644000175000017510000000000000000000000020626 0ustar00niconico00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1579961068.0 hbmqtt-0.9.6/hbmqtt/plugins/sys/broker.py0000644000175000017510000001721500000000000020373 0ustar00niconico00000000000000# Copyright (c) 2015 Nicolas JOUANIN # # See the file license.txt for copying permission. from datetime import datetime from hbmqtt.mqtt.packet import PUBLISH from hbmqtt.codecs import int_to_bytes_str import asyncio import sys from collections import deque DOLLAR_SYS_ROOT = '$SYS/broker/' STAT_BYTES_SENT = 'bytes_sent' STAT_BYTES_RECEIVED = 'bytes_received' STAT_MSG_SENT = 'messages_sent' STAT_MSG_RECEIVED = 'messages_received' STAT_PUBLISH_SENT = 'publish_sent' STAT_PUBLISH_RECEIVED = 'publish_received' STAT_START_TIME = 'start_time' STAT_CLIENTS_MAXIMUM = 'clients_maximum' STAT_CLIENTS_CONNECTED = 'clients_connected' STAT_CLIENTS_DISCONNECTED = 'clients_disconnected' class BrokerSysPlugin: def __init__(self, context): self.context = context # Broker statistics initialization self._stats = dict() self._sys_handle = None def _clear_stats(self): """ Initializes broker statistics data structures """ for stat in (STAT_BYTES_RECEIVED, STAT_BYTES_SENT, STAT_MSG_RECEIVED, STAT_MSG_SENT, STAT_CLIENTS_MAXIMUM, STAT_CLIENTS_CONNECTED, STAT_CLIENTS_DISCONNECTED, STAT_PUBLISH_RECEIVED, STAT_PUBLISH_SENT): self._stats[stat] = 0 @asyncio.coroutine def _broadcast_sys_topic(self, topic_basename, data): return (yield from self.context.broadcast_message(topic_basename, data)) def schedule_broadcast_sys_topic(self, topic_basename, data): return asyncio.ensure_future( self._broadcast_sys_topic(DOLLAR_SYS_ROOT + topic_basename, data), loop=self.context.loop ) @asyncio.coroutine def on_broker_pre_start(self, *args, **kwargs): self._clear_stats() @asyncio.coroutine def on_broker_post_start(self, *args, **kwargs): self._stats[STAT_START_TIME] = datetime.now() from hbmqtt.version import get_version version = 'HBMQTT version ' + get_version() self.context.retain_message(DOLLAR_SYS_ROOT + 'version', version.encode()) # Start $SYS topics management try: sys_interval = int(self.context.config.get('sys_interval', 0)) if sys_interval > 0: self.context.logger.debug("Setup $SYS broadcasting every %d secondes" % sys_interval) self.sys_handle = self.context.loop.call_later(sys_interval, self.broadcast_dollar_sys_topics) else: self.context.logger.debug("$SYS disabled") except KeyError: pass # 'sys_internal' config parameter not found @asyncio.coroutine def on_broker_pre_stop(self, *args, **kwargs): # Stop $SYS topics broadcasting if self.sys_handle: self.sys_handle.cancel() def broadcast_dollar_sys_topics(self): """ Broadcast dynamic $SYS topics updates and reschedule next execution depending on 'sys_interval' config parameter. """ # Update stats uptime = datetime.now() - self._stats[STAT_START_TIME] client_connected = self._stats[STAT_CLIENTS_CONNECTED] client_disconnected = self._stats[STAT_CLIENTS_DISCONNECTED] inflight_in = 0 inflight_out = 0 messages_stored = 0 for session in self.context.sessions: inflight_in += session.inflight_in_count inflight_out += session.inflight_out_count messages_stored += session.retained_messages_count messages_stored += len(self.context.retained_messages) subscriptions_count = 0 for topic in self.context.subscriptions: subscriptions_count += len(self.context.subscriptions[topic]) # Broadcast updates tasks = deque() tasks.append(self.schedule_broadcast_sys_topic('load/bytes/received', int_to_bytes_str(self._stats[STAT_BYTES_RECEIVED]))) tasks.append(self.schedule_broadcast_sys_topic('load/bytes/sent', int_to_bytes_str(self._stats[STAT_BYTES_SENT]))) tasks.append(self.schedule_broadcast_sys_topic('messages/received', int_to_bytes_str(self._stats[STAT_MSG_RECEIVED]))) tasks.append(self.schedule_broadcast_sys_topic('messages/sent', int_to_bytes_str(self._stats[STAT_MSG_SENT]))) tasks.append(self.schedule_broadcast_sys_topic('time', str(datetime.now()).encode('utf-8'))) tasks.append(self.schedule_broadcast_sys_topic('uptime', int_to_bytes_str(int(uptime.total_seconds())))) tasks.append(self.schedule_broadcast_sys_topic('uptime/formated', str(uptime).encode('utf-8'))) tasks.append(self.schedule_broadcast_sys_topic('clients/connected', int_to_bytes_str(client_connected))) tasks.append(self.schedule_broadcast_sys_topic('clients/disconnected', int_to_bytes_str(client_disconnected))) tasks.append(self.schedule_broadcast_sys_topic('clients/maximum', int_to_bytes_str(self._stats[STAT_CLIENTS_MAXIMUM]))) tasks.append(self.schedule_broadcast_sys_topic('clients/total', int_to_bytes_str(client_connected + client_disconnected))) tasks.append(self.schedule_broadcast_sys_topic('messages/inflight', int_to_bytes_str(inflight_in + inflight_out))) tasks.append(self.schedule_broadcast_sys_topic('messages/inflight/in', int_to_bytes_str(inflight_in))) tasks.append(self.schedule_broadcast_sys_topic('messages/inflight/out', int_to_bytes_str(inflight_out))) tasks.append(self.schedule_broadcast_sys_topic('messages/inflight/stored', int_to_bytes_str(messages_stored))) tasks.append(self.schedule_broadcast_sys_topic('messages/publish/received', int_to_bytes_str(self._stats[STAT_PUBLISH_RECEIVED]))) tasks.append(self.schedule_broadcast_sys_topic('messages/publish/sent', int_to_bytes_str(self._stats[STAT_PUBLISH_SENT]))) tasks.append(self.schedule_broadcast_sys_topic('messages/retained/count', int_to_bytes_str(len(self.context.retained_messages)))) tasks.append(self.schedule_broadcast_sys_topic('messages/subscriptions/count', int_to_bytes_str(subscriptions_count))) # Wait until broadcasting tasks end while tasks and tasks[0].done(): tasks.popleft() # Reschedule sys_interval = int(self.context.config['sys_interval']) self.context.logger.debug("Broadcasting $SYS topics") self.sys_handle = self.context.loop.call_later(sys_interval, self.broadcast_dollar_sys_topics) @asyncio.coroutine def on_mqtt_packet_received(self, *args, **kwargs): packet = kwargs.get('packet') if packet: packet_size = packet.bytes_length self._stats[STAT_BYTES_RECEIVED] += packet_size self._stats[STAT_MSG_RECEIVED] += 1 if packet.fixed_header.packet_type == PUBLISH: self._stats[STAT_PUBLISH_RECEIVED] += 1 @asyncio.coroutine def on_mqtt_packet_sent(self, *args, **kwargs): packet = kwargs.get('packet') if packet: packet_size = packet.bytes_length self._stats[STAT_BYTES_SENT] += packet_size self._stats[STAT_MSG_SENT] += 1 if packet.fixed_header.packet_type == PUBLISH: self._stats[STAT_PUBLISH_SENT] += 1 @asyncio.coroutine def on_broker_client_connected(self, *args, **kwargs): self._stats[STAT_CLIENTS_CONNECTED] += 1 self._stats[STAT_CLIENTS_MAXIMUM] = max(self._stats[STAT_CLIENTS_MAXIMUM], self._stats[STAT_CLIENTS_CONNECTED]) @asyncio.coroutine def on_broker_client_disconnected(self, *args, **kwargs): self._stats[STAT_CLIENTS_CONNECTED] -= 1 self._stats[STAT_CLIENTS_DISCONNECTED] += 1 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1579961068.0 hbmqtt-0.9.6/hbmqtt/plugins/topic_checking.py0000644000175000017510000000542200000000000021237 0ustar00niconico00000000000000import asyncio class BaseTopicPlugin: def __init__(self, context): self.context = context try: self.topic_config = self.context.config['topic-check'] except KeyError: self.context.logger.warning("'topic-check' section not found in context configuration") def topic_filtering(self, *args, **kwargs): if not self.topic_config: # auth config section not found self.context.logger.warning("'auth' section not found in context configuration") return False return True class TopicTabooPlugin(BaseTopicPlugin): def __init__(self, context): super().__init__(context) self._taboo = ['prohibited', 'top-secret', 'data/classified'] @asyncio.coroutine def topic_filtering(self, *args, **kwargs): filter_result = super().topic_filtering(*args, **kwargs) if filter_result: session = kwargs.get('session', None) topic = kwargs.get('topic', None) if topic: if session.username != 'admin' and topic in self._taboo: return False return True else: return False return filter_result class TopicAccessControlListPlugin(BaseTopicPlugin): def __init__(self, context): super().__init__(context) @staticmethod def topic_ac(topic_requested, topic_allowed): req_split = topic_requested.split('/') allowed_split = topic_allowed.split('/') ret = True for i in range(max(len(req_split), len(allowed_split))): try: a_aux = req_split[i] b_aux = allowed_split[i] except IndexError: ret = False break if b_aux == '#': break elif (b_aux == '+') or (b_aux == a_aux): continue else: ret = False break return ret @asyncio.coroutine def topic_filtering(self, *args, **kwargs): filter_result = super().topic_filtering(*args, **kwargs) if filter_result: session = kwargs.get('session', None) req_topic = kwargs.get('topic', None) if req_topic: username = session.username if username is None: username = 'anonymous' allowed_topics = self.topic_config['acl'].get(username, None) if allowed_topics: for allowed_topic in allowed_topics: if self.topic_ac(req_topic, allowed_topic): return True return False else: return False else: return False ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1579961068.0 hbmqtt-0.9.6/hbmqtt/session.py0000644000175000017510000001624300000000000016273 0ustar00niconico00000000000000# Copyright (c) 2015 Nicolas JOUANIN # # See the file license.txt for copying permission. import asyncio from transitions import Machine from asyncio import Queue from collections import OrderedDict from hbmqtt.mqtt.publish import PublishPacket from hbmqtt.errors import HBMQTTException OUTGOING = 0 INCOMING = 1 class ApplicationMessage: """ ApplicationMessage and subclasses are used to store published message information flow. These objects can contain different information depending on the way they were created (incoming or outgoing) and the quality of service used between peers. """ __slots__ = ( 'packet_id', 'topic', 'qos', 'data', 'retain', 'publish_packet', 'puback_packet', 'pubrec_packet', 'pubrel_packet', 'pubcomp_packet', ) def __init__(self, packet_id, topic, qos, data, retain): self.packet_id = packet_id """ Publish message `packet identifier `_""" self.topic = topic """ Publish message topic""" self.qos = qos """ Publish message Quality of Service""" self.data = data """ Publish message payload data""" self.retain = retain """ Publish message retain flag""" self.publish_packet = None """ :class:`hbmqtt.mqtt.publish.PublishPacket` instance corresponding to the `PUBLISH `_ packet in the messages flow. ``None`` if the PUBLISH packet has not already been received or sent.""" self.puback_packet = None """ :class:`hbmqtt.mqtt.puback.PubackPacket` instance corresponding to the `PUBACK `_ packet in the messages flow. ``None`` if QoS != QOS_1 or if the PUBACK packet has not already been received or sent.""" self.pubrec_packet = None """ :class:`hbmqtt.mqtt.puback.PubrecPacket` instance corresponding to the `PUBREC `_ packet in the messages flow. ``None`` if QoS != QOS_2 or if the PUBREC packet has not already been received or sent.""" self.pubrel_packet = None """ :class:`hbmqtt.mqtt.puback.PubrelPacket` instance corresponding to the `PUBREL `_ packet in the messages flow. ``None`` if QoS != QOS_2 or if the PUBREL packet has not already been received or sent.""" self.pubcomp_packet = None """ :class:`hbmqtt.mqtt.puback.PubrelPacket` instance corresponding to the `PUBCOMP `_ packet in the messages flow. ``None`` if QoS != QOS_2 or if the PUBCOMP packet has not already been received or sent.""" def build_publish_packet(self, dup=False): """ Build :class:`hbmqtt.mqtt.publish.PublishPacket` from attributes :param dup: force dup flag :return: :class:`hbmqtt.mqtt.publish.PublishPacket` built from ApplicationMessage instance attributes """ return PublishPacket.build(self.topic, self.data, self.packet_id, dup, self.qos, self.retain) def __eq__(self, other): return self.packet_id == other.packet_id class IncomingApplicationMessage(ApplicationMessage): """ Incoming :class:`~hbmqtt.session.ApplicationMessage`. """ __slots__ = ('direction',) def __init__(self, packet_id, topic, qos, data, retain): super().__init__(packet_id, topic, qos, data, retain) self.direction = INCOMING class OutgoingApplicationMessage(ApplicationMessage): """ Outgoing :class:`~hbmqtt.session.ApplicationMessage`. """ __slots__ = ('direction',) def __init__(self, packet_id, topic, qos, data, retain): super().__init__(packet_id, topic, qos, data, retain) self.direction = OUTGOING class Session: states = ['new', 'connected', 'disconnected'] def __init__(self, loop=None): self._init_states() self.remote_address = None self.remote_port = None self.client_id = None self.clean_session = None self.will_flag = False self.will_message = None self.will_qos = None self.will_retain = None self.will_topic = None self.keep_alive = 0 self.publish_retry_delay = 0 self.broker_uri = None self.username = None self.password = None self.cafile = None self.capath = None self.cadata = None self._packet_id = 0 self.parent = 0 if loop is not None: self._loop = loop else: self._loop = asyncio.get_event_loop() # Used to store outgoing ApplicationMessage while publish protocol flows self.inflight_out = OrderedDict() # Used to store incoming ApplicationMessage while publish protocol flows self.inflight_in = OrderedDict() # Stores messages retained for this session self.retained_messages = Queue(loop=self._loop) # Stores PUBLISH messages ID received in order and ready for application process self.delivered_message_queue = Queue(loop=self._loop) def _init_states(self): self.transitions = Machine(states=Session.states, initial='new') self.transitions.add_transition(trigger='connect', source='new', dest='connected') self.transitions.add_transition(trigger='connect', source='disconnected', dest='connected') self.transitions.add_transition(trigger='disconnect', source='connected', dest='disconnected') self.transitions.add_transition(trigger='disconnect', source='new', dest='disconnected') self.transitions.add_transition(trigger='disconnect', source='disconnected', dest='disconnected') @property def next_packet_id(self): self._packet_id += 1 if self._packet_id > 65535: self._packet_id = 1 while self._packet_id in self.inflight_in or self._packet_id in self.inflight_out: self._packet_id += 1 if self._packet_id > 65535: raise HBMQTTException("More than 65525 messages pending. No free packet ID") return self._packet_id @property def inflight_in_count(self): return len(self.inflight_in) @property def inflight_out_count(self): return len(self.inflight_out) @property def retained_messages_count(self): return self.retained_messages.qsize() def __repr__(self): return type(self).__name__ + '(clientId={0}, state={1})'.format(self.client_id, self.transitions.state) def __getstate__(self): state = self.__dict__.copy() # Remove the unpicklable entries. # del state['transitions'] del state['retained_messages'] del state['delivered_message_queue'] return state def __setstate(self, state): self.__dict__.update(state) self.retained_messages = Queue() self.delivered_message_queue = Queue() def __eq__(self, other): return self.client_id == other.client_id ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1579961068.0 hbmqtt-0.9.6/hbmqtt/utils.py0000644000175000017510000000244000000000000015742 0ustar00niconico00000000000000# Copyright (c) 2015 Nicolas JOUANIN # # See the file license.txt for copying permission. import logging import yaml logger = logging.getLogger(__name__) def not_in_dict_or_none(dict, key): """ Check if a key exists in a map and if it's not None :param dict: map to look for key :param key: key to find :return: true if key is in dict and not None """ if key not in dict or dict[key] is None: return True else: return False def format_client_message(session=None, address=None, port=None): if session: return "(client id=%s)" % session.client_id elif address is not None and port is not None: return "(client @=%s:%d)" % (address, port) else: return "(unknown client)" def gen_client_id(): """ Generates random client ID :return: """ import random gen_id = 'hbmqtt/' for i in range(7, 23): gen_id += chr(random.randint(0, 74) + 48) return gen_id def read_yaml_config(config_file): config = None try: with open(config_file, 'r') as stream: config = yaml.full_load(stream) if hasattr(yaml, 'full_load') else yaml.load(stream) except yaml.YAMLError as exc: logger.error("Invalid config_file %s: %s" % (config_file, exc)) return config ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1579961068.0 hbmqtt-0.9.6/hbmqtt/version.py0000644000175000017510000000355400000000000016276 0ustar00niconico00000000000000# Copyright (c) 2015 Nicolas JOUANIN # # See the file license.txt for copying permission. import datetime import os import subprocess # Version Management from https://gist.github.com/gilsondev/2790884 def get_version(version=None): "Returns a PEP 386-compliant version number from VERSION." if version is None: from hbmqtt import VERSION as version else: assert len(version) == 5 assert version[3] in ('alpha', 'beta', 'rc', 'final') # Now build the two parts of the version number: # main = X.Y[.Z] # sub = .devN - for pre-alpha releases # | {a|b|c}N - for alpha, beta and rc releases parts = 2 if version[2] == 0 else 3 main = '.'.join(str(x) for x in version[:parts]) sub = '' if version[3] == 'alpha' and version[4] == 0: git_changeset = get_git_changeset() if git_changeset: sub = '.dev%s' % git_changeset elif version[3] != 'final': mapping = {'alpha': 'a', 'beta': 'b', 'rc': 'c'} sub = mapping[version[3]] + str(version[4]) return str(main + sub) def get_git_changeset(): """Returns a numeric identifier of the latest git changeset. The result is the UTC timestamp of the changeset in YYYYMMDDHHMMSS format. This value isn't guaranteed to be unique, but collisions are very unlikely, so it's sufficient for generating the development version numbers. """ repo_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) git_log = subprocess.Popen( 'git log --pretty=format:%ct --quiet -1 HEAD', stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True, cwd=repo_dir, universal_newlines=True) timestamp = git_log.communicate()[0] try: timestamp = datetime.datetime.utcfromtimestamp(int(timestamp)) except ValueError: return None return timestamp.strftime('%Y%m%d%H%M%S') ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1579961563.6901383 hbmqtt-0.9.6/hbmqtt.egg-info/0000755000175000017510000000000000000000000015722 5ustar00niconico00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1579961563.0 hbmqtt-0.9.6/hbmqtt.egg-info/PKG-INFO0000644000175000017510000000135700000000000017025 0ustar00niconico00000000000000Metadata-Version: 1.1 Name: hbmqtt Version: 0.9.6 Summary: MQTT client/broker using Python 3.4 asyncio library Home-page: https://github.com/beerfactory/hbmqtt Author: Nicolas Jouanin Author-email: nico@beerfactory.org License: MIT Description: UNKNOWN Platform: all Classifier: Development Status :: 3 - Alpha Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: MIT License Classifier: Operating System :: POSIX Classifier: Operating System :: MacOS Classifier: Operating System :: Microsoft :: Windows Classifier: Programming Language :: Python :: 3.4 Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: 3.6 Classifier: Topic :: Communications Classifier: Topic :: Internet ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1579961563.0 hbmqtt-0.9.6/hbmqtt.egg-info/SOURCES.txt0000644000175000017510000000512000000000000017604 0ustar00niconico00000000000000MANIFEST.in license.txt readme.rst setup.cfg setup.py tox.ini docs/Makefile docs/changelog.rst docs/conf.py docs/index.rst docs/license.rst docs/quickstart.rst docs/_static/theme_overrides.css docs/_templates/layout.html docs/references/broker.rst docs/references/common.rst docs/references/hbmqtt.rst docs/references/hbmqtt_pub.rst docs/references/hbmqtt_sub.rst docs/references/index.rst docs/references/mqttclient.rst hbmqtt/__init__.py hbmqtt/adapters.py hbmqtt/broker.py hbmqtt/client.py hbmqtt/codecs.py hbmqtt/errors.py hbmqtt/session.py hbmqtt/utils.py hbmqtt/version.py hbmqtt.egg-info/PKG-INFO hbmqtt.egg-info/SOURCES.txt hbmqtt.egg-info/dependency_links.txt hbmqtt.egg-info/entry_points.txt hbmqtt.egg-info/requires.txt hbmqtt.egg-info/top_level.txt hbmqtt/mqtt/__init__.py hbmqtt/mqtt/connack.py hbmqtt/mqtt/connect.py hbmqtt/mqtt/constants.py hbmqtt/mqtt/disconnect.py hbmqtt/mqtt/packet.py hbmqtt/mqtt/pingreq.py hbmqtt/mqtt/pingresp.py hbmqtt/mqtt/puback.py hbmqtt/mqtt/pubcomp.py hbmqtt/mqtt/publish.py hbmqtt/mqtt/pubrec.py hbmqtt/mqtt/pubrel.py hbmqtt/mqtt/suback.py hbmqtt/mqtt/subscribe.py hbmqtt/mqtt/unsuback.py hbmqtt/mqtt/unsubscribe.py hbmqtt/mqtt/protocol/__init__.py hbmqtt/mqtt/protocol/broker_handler.py hbmqtt/mqtt/protocol/client_handler.py hbmqtt/mqtt/protocol/handler.py hbmqtt/plugins/__init__.py hbmqtt/plugins/authentication.py hbmqtt/plugins/logging.py hbmqtt/plugins/manager.py hbmqtt/plugins/persistence.py hbmqtt/plugins/topic_checking.py hbmqtt/plugins/sys/__init__.py hbmqtt/plugins/sys/broker.py samples/broker_acl.py samples/broker_start.py samples/broker_taboo.py samples/client_keepalive.py samples/client_publish.py samples/client_publish_acl.py samples/client_publish_ssl.py samples/client_publish_ws.py samples/client_subscribe.py samples/client_subscribe_acl.py samples/mosquitto.org.crt samples/passwd scripts/__init__.py scripts/broker_script.py scripts/default_broker.yaml scripts/default_client.yaml scripts/pub_script.py scripts/sub_script.py tests/__init__.py tests/mosquitto.org.crt tests/test_broker.py tests/test_client.py tests/test_codecs.py tests/mqtt/__init__.py tests/mqtt/test_connect.py tests/mqtt/test_packet.py tests/mqtt/test_puback.py tests/mqtt/test_pubcomp.py tests/mqtt/test_publish.py tests/mqtt/test_pubrec.py tests/mqtt/test_pubrel.py tests/mqtt/test_suback.py tests/mqtt/test_subscribe.py tests/mqtt/test_unsuback.py tests/mqtt/test_unsubscribe.py tests/mqtt/protocol/__init__.py tests/mqtt/protocol/test_handler.py tests/plugins/__init__.py tests/plugins/passwd tests/plugins/test_authentication.py tests/plugins/test_manager.py tests/plugins/test_persistence.py././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1579961563.0 hbmqtt-0.9.6/hbmqtt.egg-info/dependency_links.txt0000644000175000017510000000000100000000000021770 0ustar00niconico00000000000000 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1579961563.0 hbmqtt-0.9.6/hbmqtt.egg-info/entry_points.txt0000644000175000017510000000146200000000000021223 0ustar00niconico00000000000000[console_scripts] hbmqtt = scripts.broker_script:main hbmqtt_pub = scripts.pub_script:main hbmqtt_sub = scripts.sub_script:main [hbmqtt.broker.plugins] auth_anonymous = hbmqtt.plugins.authentication:AnonymousAuthPlugin auth_file = hbmqtt.plugins.authentication:FileAuthPlugin broker_sys = hbmqtt.plugins.sys.broker:BrokerSysPlugin packet_logger_plugin = hbmqtt.plugins.logging:PacketLoggerPlugin topic_acl = hbmqtt.plugins.topic_checking:TopicAccessControlListPlugin topic_taboo = hbmqtt.plugins.topic_checking:TopicTabooPlugin [hbmqtt.client.plugins] packet_logger_plugin = hbmqtt.plugins.logging:PacketLoggerPlugin [hbmqtt.test.plugins] event_plugin = tests.plugins.test_manager:EventTestPlugin packet_logger_plugin = hbmqtt.plugins.logging:PacketLoggerPlugin test_plugin = tests.plugins.test_manager:TestPlugin ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1579961563.0 hbmqtt-0.9.6/hbmqtt.egg-info/requires.txt0000644000175000017510000000005500000000000020322 0ustar00niconico00000000000000transitions websockets passlib docopt pyyaml ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1579961563.0 hbmqtt-0.9.6/hbmqtt.egg-info/top_level.txt0000644000175000017510000000002500000000000020451 0ustar00niconico00000000000000hbmqtt scripts tests ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1579961068.0 hbmqtt-0.9.6/license.txt0000644000175000017510000000207200000000000015115 0ustar00niconico00000000000000The MIT License (MIT) Copyright (c) 2015 Nicolas JOUANIN Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1579961068.0 hbmqtt-0.9.6/readme.rst0000644000175000017510000000426200000000000014724 0ustar00niconico00000000000000HBMQTT ====== ``HBMQTT`` is an open source `MQTT`_ client and broker implementation. Built on top of `asyncio`_, Python's standard asynchronous I/O framework, HBMQTT provides a straightforward API based on coroutines, making it easy to write highly concurrent applications. .. _asyncio: https://docs.python.org/3/library/asyncio.html Features -------- HBMQTT implements the full set of `MQTT 3.1.1`_ protocol specifications and provides the following features: - Support QoS 0, QoS 1 and QoS 2 messages flow - Client auto-reconnection on network lost - Authentication through password file (more methods can be added through a plugin system) - Basic ``$SYS`` topics - TCP and websocket support - SSL support over TCP and websocket - Plugin system Build status ------------ .. image:: https://travis-ci.org/beerfactory/hbmqtt.svg?branch=master :target: https://travis-ci.org/beerfactory/hbmqtt .. image:: https://coveralls.io/repos/beerfactory/hbmqtt/badge.svg?branch=master&service=github :target: https://coveralls.io/github/beerfactory/hbmqtt?branch=master Project status -------------- .. image:: https://readthedocs.org/projects/hbmqtt/badge/?version=latest :target: http://hbmqtt.readthedocs.org/en/latest/?badge=latest :alt: Documentation Status .. image:: https://badge.fury.io/py/hbmqtt.svg :target: https://badge.fury.io/py/hbmqtt Getting started --------------- hbmqtt is available on `Pypi `_ and can installed simply using ``pip`` : :: $ pip install hbmqtt Documentation is available on `Read the Docs`_. Bug reports, patches and suggestions welcome! Just `open an issue`_ or join the `gitter channel`_. .. image:: https://badges.gitter.im/Join%20Chat.svg :target: https://gitter.im/beerfactory/hbmqtt?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge :alt: 'Join the chat at https://gitter.im/beerfactory/hbmqtt' .. _MQTT: http://www.mqtt.org .. _MQTT 3.1.1: http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html .. _Read the Docs: http://hbmqtt.readthedocs.org/ .. _open an issue: https://github.com/beerfactory/hbmqtt/issues/new .. _gitter channel: https://gitter.im/beerfactory/hbmqtt ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1579961563.6934717 hbmqtt-0.9.6/samples/0000755000175000017510000000000000000000000014375 5ustar00niconico00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1579961068.0 hbmqtt-0.9.6/samples/broker_acl.py0000644000175000017510000000237200000000000017056 0ustar00niconico00000000000000import logging import asyncio import os from hbmqtt.broker import Broker logger = logging.getLogger(__name__) config = { 'listeners': { 'default': { 'type': 'tcp', 'bind': '0.0.0.0:1883', }, 'ws-mqtt': { 'bind': '127.0.0.1:8080', 'type': 'ws', 'max_connections': 10, }, }, 'sys_interval': 10, 'auth': { 'allow-anonymous': True, 'password-file': os.path.join(os.path.dirname(os.path.realpath(__file__)), "passwd"), 'plugins': [ 'auth_file', 'auth_anonymous' ] }, 'topic-check': { 'enabled': True, 'plugins': [ 'topic_acl' ], 'acl': { # username: [list of allowed topics] 'test': ['repositories/+/master', 'calendar/#', 'data/memes'], 'anonymous': [] } } } broker = Broker(config) @asyncio.coroutine def test_coro(): yield from broker.start() if __name__ == '__main__': formatter = "[%(asctime)s] :: %(levelname)s :: %(name)s :: %(message)s" logging.basicConfig(level=logging.INFO, format=formatter) asyncio.get_event_loop().run_until_complete(test_coro()) asyncio.get_event_loop().run_forever() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1579961068.0 hbmqtt-0.9.6/samples/broker_start.py0000644000175000017510000000222000000000000017444 0ustar00niconico00000000000000import logging import asyncio import os from hbmqtt.broker import Broker logger = logging.getLogger(__name__) config = { 'listeners': { 'default': { 'type': 'tcp', 'bind': '0.0.0.0:1883', }, 'ws-mqtt': { 'bind': '127.0.0.1:8080', 'type': 'ws', 'max_connections': 10, }, }, 'sys_interval': 10, 'auth': { 'allow-anonymous': True, 'password-file': os.path.join(os.path.dirname(os.path.realpath(__file__)), "passwd"), 'plugins': [ 'auth_file', 'auth_anonymous' ] }, 'topic-check': { 'enabled': False } } broker = Broker(config) @asyncio.coroutine def test_coro(): yield from broker.start() #yield from asyncio.sleep(5) #yield from broker.shutdown() if __name__ == '__main__': formatter = "[%(asctime)s] :: %(levelname)s :: %(name)s :: %(message)s" #formatter = "%(asctime)s :: %(levelname)s :: %(message)s" logging.basicConfig(level=logging.INFO, format=formatter) asyncio.get_event_loop().run_until_complete(test_coro()) asyncio.get_event_loop().run_forever() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1579961068.0 hbmqtt-0.9.6/samples/broker_taboo.py0000644000175000017510000000211000000000000017411 0ustar00niconico00000000000000import logging import asyncio import os from hbmqtt.broker import Broker logger = logging.getLogger(__name__) config = { 'listeners': { 'default': { 'type': 'tcp', 'bind': '0.0.0.0:1883', }, 'ws-mqtt': { 'bind': '127.0.0.1:8080', 'type': 'ws', 'max_connections': 10, }, }, 'sys_interval': 10, 'auth': { 'allow-anonymous': True, 'password-file': os.path.join(os.path.dirname(os.path.realpath(__file__)), "passwd"), 'plugins': [ 'auth_file', 'auth_anonymous' ] }, 'topic-check': { 'enabled': True, 'plugins': [ 'topic_taboo' ] } } broker = Broker(config) @asyncio.coroutine def test_coro(): yield from broker.start() if __name__ == '__main__': formatter = "[%(asctime)s] :: %(levelname)s :: %(name)s :: %(message)s" logging.basicConfig(level=logging.INFO, format=formatter) asyncio.get_event_loop().run_until_complete(test_coro()) asyncio.get_event_loop().run_forever() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1579961068.0 hbmqtt-0.9.6/samples/client_keepalive.py0000644000175000017510000000132000000000000020246 0ustar00niconico00000000000000import logging import asyncio from hbmqtt.client import MQTTClient # # This sample shows a client running idle. # Meanwhile, keepalive is managed through PING messages sent every 5 seconds # logger = logging.getLogger(__name__) config = { 'keep_alive': 5, 'ping_delay': 1, } C = MQTTClient(config=config) @asyncio.coroutine def test_coro(): yield from C.connect('mqtt://test.mosquitto.org:1883/') yield from asyncio.sleep(18) yield from C.disconnect() if __name__ == '__main__': formatter = "[%(asctime)s] {%(filename)s:%(lineno)d} %(levelname)s - %(message)s" logging.basicConfig(level=logging.DEBUG, format=formatter) asyncio.get_event_loop().run_until_complete(test_coro()) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1579961068.0 hbmqtt-0.9.6/samples/client_publish.py0000644000175000017510000000344600000000000017762 0ustar00niconico00000000000000import logging import asyncio from hbmqtt.client import MQTTClient, ConnectException from hbmqtt.mqtt.constants import QOS_1, QOS_2 # # This sample shows how to publish messages to broker using different QOS # Debug outputs shows the message flows # logger = logging.getLogger(__name__) config = { 'will': { 'topic': '/will/client', 'message': b'Dead or alive', 'qos': 0x01, 'retain': True } } @asyncio.coroutine def test_coro(): C = MQTTClient() yield from C.connect('mqtt://test.mosquitto.org/') tasks = [ asyncio.ensure_future(C.publish('a/b', b'TEST MESSAGE WITH QOS_0')), asyncio.ensure_future(C.publish('a/b', b'TEST MESSAGE WITH QOS_1', qos=QOS_1)), asyncio.ensure_future(C.publish('a/b', b'TEST MESSAGE WITH QOS_2', qos=QOS_2)), ] yield from asyncio.wait(tasks) logger.info("messages published") yield from C.disconnect() @asyncio.coroutine def test_coro2(): try: C = MQTTClient() yield from C.connect('mqtt://test.mosquitto.org:1883/') yield from C.publish('a/b', b'TEST MESSAGE WITH QOS_0', qos=0x00) yield from C.publish('a/b', b'TEST MESSAGE WITH QOS_1', qos=0x01) yield from C.publish('a/b', b'TEST MESSAGE WITH QOS_2', qos=0x02) logger.info("messages published") yield from C.disconnect() except ConnectException as ce: logger.error("Connection failed: %s" % ce) asyncio.get_event_loop().stop() if __name__ == '__main__': formatter = "[%(asctime)s] %(name)s {%(filename)s:%(lineno)d} %(levelname)s - %(message)s" formatter = "%(message)s" logging.basicConfig(level=logging.DEBUG, format=formatter) asyncio.get_event_loop().run_until_complete(test_coro()) asyncio.get_event_loop().run_until_complete(test_coro2()) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1579961068.0 hbmqtt-0.9.6/samples/client_publish_acl.py0000644000175000017510000000235300000000000020575 0ustar00niconico00000000000000import logging import asyncio from hbmqtt.client import MQTTClient, ConnectException # # This sample shows how to publish messages to broker using different QOS # Debug outputs shows the message flows # logger = logging.getLogger(__name__) @asyncio.coroutine def test_coro(): try: C = MQTTClient() yield from C.connect('mqtt://0.0.0.0:1883') yield from C.publish('data/classified', b'TOP SECRET', qos=0x01) yield from C.publish('data/memes', b'REAL FUN', qos=0x01) yield from C.publish('repositories/hbmqtt/master', b'NEW STABLE RELEASE', qos=0x01) yield from C.publish('repositories/hbmqtt/devel', b'THIS NEEDS TO BE CHECKED', qos=0x01) yield from C.publish('calendar/hbmqtt/releases', b'NEW RELEASE', qos=0x01) logger.info("messages published") yield from C.disconnect() except ConnectException as ce: logger.error("Connection failed: %s" % ce) asyncio.get_event_loop().stop() if __name__ == '__main__': formatter = "[%(asctime)s] %(name)s {%(filename)s:%(lineno)d} %(levelname)s - %(message)s" formatter = "%(message)s" logging.basicConfig(level=logging.DEBUG, format=formatter) asyncio.get_event_loop().run_until_complete(test_coro()) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1579961068.0 hbmqtt-0.9.6/samples/client_publish_ssl.py0000644000175000017510000000227000000000000020635 0ustar00niconico00000000000000import logging import asyncio from hbmqtt.client import MQTTClient from hbmqtt.mqtt.constants import QOS_1, QOS_2 # # This sample shows how to publish messages to broker using different QOS # Debug outputs shows the message flows # logger = logging.getLogger(__name__) config = { 'will': { 'topic': '/will/client', 'message': b'Dead or alive', 'qos': 0x01, 'retain': True, }, } C = MQTTClient(config=config) #C = MQTTClient() @asyncio.coroutine def test_coro(): yield from C.connect('mqtts://test.mosquitto.org/', cafile='mosquitto.org.crt') tasks = [ asyncio.ensure_future(C.publish('a/b', b'TEST MESSAGE WITH QOS_0')), asyncio.ensure_future(C.publish('a/b', b'TEST MESSAGE WITH QOS_1', qos=QOS_1)), asyncio.ensure_future(C.publish('a/b', b'TEST MESSAGE WITH QOS_2', qos=QOS_2)), ] yield from asyncio.wait(tasks) logger.info("messages published") yield from C.disconnect() if __name__ == '__main__': formatter = "[%(asctime)s] {%(filename)s:%(lineno)d} %(levelname)s - %(message)s" logging.basicConfig(level=logging.DEBUG, format=formatter) asyncio.get_event_loop().run_until_complete(test_coro()) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1579961068.0 hbmqtt-0.9.6/samples/client_publish_ws.py0000644000175000017510000000231500000000000020465 0ustar00niconico00000000000000import logging import asyncio from hbmqtt.client import MQTTClient from hbmqtt.mqtt.constants import QOS_1, QOS_2 # # This sample shows how to publish messages to broker using different QOS # Debug outputs shows the message flows # logger = logging.getLogger(__name__) config = { 'will': { 'topic': '/will/client', 'message': b'Dead or alive', 'qos': 0x01, 'retain': True }, 'capath': '.', } C = MQTTClient(config=config) #C = MQTTClient() @asyncio.coroutine def test_coro(): yield from C.connect('wss://test.mosquitto.org:8081/', cafile='mosquitto.org.crt') tasks = [ asyncio.ensure_future(C.publish('a/b', b'TEST MESSAGE WITH QOS_0')), asyncio.ensure_future(C.publish('a/b', b'TEST MESSAGE WITH QOS_1', qos=QOS_1)), asyncio.ensure_future(C.publish('a/b', b'TEST MESSAGE WITH QOS_2', qos=QOS_2)), ] yield from asyncio.wait(tasks) logger.info("messages published") yield from C.disconnect() if __name__ == '__main__': formatter = "[%(asctime)s] {%(filename)s:%(lineno)d} %(levelname)s - %(message)s" logging.basicConfig(level=logging.DEBUG, format=formatter) asyncio.get_event_loop().run_until_complete(test_coro()) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1579961068.0 hbmqtt-0.9.6/samples/client_subscribe.py0000644000175000017510000000255700000000000020277 0ustar00niconico00000000000000import logging import asyncio from hbmqtt.client import MQTTClient, ClientException from hbmqtt.mqtt.constants import QOS_1, QOS_2 # # This sample shows how to subscbribe a topic and receive data from incoming messages # It subscribes to '$SYS/broker/uptime' topic and displays the first ten values returned # by the broker. # logger = logging.getLogger(__name__) @asyncio.coroutine def uptime_coro(): C = MQTTClient() yield from C.connect('mqtt://test.mosquitto.org/') # Subscribe to '$SYS/broker/uptime' with QOS=1 yield from C.subscribe([ ('$SYS/broker/uptime', QOS_1), ('$SYS/broker/load/#', QOS_2), ]) logger.info("Subscribed") try: for i in range(1, 100): message = yield from C.deliver_message() packet = message.publish_packet print("%d: %s => %s" % (i, packet.variable_header.topic_name, str(packet.payload.data))) yield from C.unsubscribe(['$SYS/broker/uptime', '$SYS/broker/load/#']) logger.info("UnSubscribed") yield from C.disconnect() except ClientException as ce: logger.error("Client exception: %s" % ce) if __name__ == '__main__': formatter = "[%(asctime)s] {%(filename)s:%(lineno)d} %(levelname)s - %(message)s" logging.basicConfig(level=logging.INFO, format=formatter) asyncio.get_event_loop().run_until_complete(uptime_coro()) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1579961068.0 hbmqtt-0.9.6/samples/client_subscribe_acl.py0000644000175000017510000000316500000000000021112 0ustar00niconico00000000000000import logging import asyncio from hbmqtt.client import MQTTClient, ClientException from hbmqtt.mqtt.constants import QOS_1 # # This sample shows how to subscbribe a topic and receive data from incoming messages # It subscribes to '$SYS/broker/uptime' topic and displays the first ten values returned # by the broker. # logger = logging.getLogger(__name__) @asyncio.coroutine def uptime_coro(): C = MQTTClient() yield from C.connect('mqtt://test:test@0.0.0.0:1883') # yield from C.connect('mqtt://0.0.0.0:1883') # Subscribe to '$SYS/broker/uptime' with QOS=1 yield from C.subscribe([ ('data/memes', QOS_1), # Topic allowed ('data/classified', QOS_1), # Topic forbidden ('repositories/hbmqtt/master', QOS_1), # Topic allowed ('repositories/hbmqtt/devel', QOS_1), # Topic forbidden ('calendar/hbmqtt/releases', QOS_1), # Topic allowed ]) logger.info("Subscribed") try: for i in range(1, 100): message = yield from C.deliver_message() packet = message.publish_packet print("%d: %s => %s" % (i, packet.variable_header.topic_name, str(packet.payload.data))) yield from C.unsubscribe(['$SYS/broker/uptime', '$SYS/broker/load/#']) logger.info("UnSubscribed") yield from C.disconnect() except ClientException as ce: logger.error("Client exception: %s" % ce) if __name__ == '__main__': formatter = "[%(asctime)s] {%(filename)s:%(lineno)d} %(levelname)s - %(message)s" logging.basicConfig(level=logging.INFO, format=formatter) asyncio.get_event_loop().run_until_complete(uptime_coro()) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1579961068.0 hbmqtt-0.9.6/samples/mosquitto.org.crt0000644000175000017510000000206600000000000017745 0ustar00niconico00000000000000-----BEGIN CERTIFICATE----- MIIC8DCCAlmgAwIBAgIJAOD63PlXjJi8MA0GCSqGSIb3DQEBBQUAMIGQMQswCQYD VQQGEwJHQjEXMBUGA1UECAwOVW5pdGVkIEtpbmdkb20xDjAMBgNVBAcMBURlcmJ5 MRIwEAYDVQQKDAlNb3NxdWl0dG8xCzAJBgNVBAsMAkNBMRYwFAYDVQQDDA1tb3Nx dWl0dG8ub3JnMR8wHQYJKoZIhvcNAQkBFhByb2dlckBhdGNob28ub3JnMB4XDTEy MDYyOTIyMTE1OVoXDTIyMDYyNzIyMTE1OVowgZAxCzAJBgNVBAYTAkdCMRcwFQYD VQQIDA5Vbml0ZWQgS2luZ2RvbTEOMAwGA1UEBwwFRGVyYnkxEjAQBgNVBAoMCU1v c3F1aXR0bzELMAkGA1UECwwCQ0ExFjAUBgNVBAMMDW1vc3F1aXR0by5vcmcxHzAd BgkqhkiG9w0BCQEWEHJvZ2VyQGF0Y2hvby5vcmcwgZ8wDQYJKoZIhvcNAQEBBQAD gY0AMIGJAoGBAMYkLmX7SqOT/jJCZoQ1NWdCrr/pq47m3xxyXcI+FLEmwbE3R9vM rE6sRbP2S89pfrCt7iuITXPKycpUcIU0mtcT1OqxGBV2lb6RaOT2gC5pxyGaFJ+h A+GIbdYKO3JprPxSBoRponZJvDGEZuM3N7p3S/lRoi7G5wG5mvUmaE5RAgMBAAGj UDBOMB0GA1UdDgQWBBTad2QneVztIPQzRRGj6ZHKqJTv5jAfBgNVHSMEGDAWgBTa d2QneVztIPQzRRGj6ZHKqJTv5jAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBQUA A4GBAAqw1rK4NlRUCUBLhEFUQasjP7xfFqlVbE2cRy0Rs4o3KS0JwzQVBwG85xge REyPOFdGdhBY2P1FNRy0MDr6xr+D2ZOwxs63dG1nnAnWZg7qwoLgpZ4fESPD3PkA 1ZgKJc2zbSQ9fCPxt2W3mdVav66c6fsb7els2W2Iz7gERJSX -----END CERTIFICATE----- ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1579961068.0 hbmqtt-0.9.6/samples/passwd0000644000175000017510000000024700000000000015624 0ustar00niconico00000000000000# Test user with 'test' password encrypted with sha-512 test:$6$l4zQEHEcowc1Pnv4$HHrh8xnsZoLItQ8BmpFHM4r6q5UqK3DnXp2GaTm5zp5buQ7NheY3Xt9f6godVKbEtA.hOC7IEDwnok3pbAOip.././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1579961563.6934717 hbmqtt-0.9.6/scripts/0000755000175000017510000000000000000000000014420 5ustar00niconico00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1579961068.0 hbmqtt-0.9.6/scripts/__init__.py0000644000175000017510000000000000000000000016517 0ustar00niconico00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1579961068.0 hbmqtt-0.9.6/scripts/broker_script.py0000644000175000017510000000403000000000000017637 0ustar00niconico00000000000000# Copyright (c) 2015 Nicolas JOUANIN # # See the file license.txt for copying permission. """ HBMQTT - MQTT 3.1.1 broker Usage: hbmqtt --version hbmqtt (-h | --help) hbmqtt [-c ] [-d] Options: -h --help Show this screen. --version Show version. -c Broker configuration file (YAML format) -d Enable debug messages """ import sys import logging import asyncio import os from hbmqtt.broker import Broker from hbmqtt.version import get_version from docopt import docopt from hbmqtt.utils import read_yaml_config default_config = { 'listeners': { 'default': { 'type': 'tcp', 'bind': '0.0.0.0:1883', }, }, 'sys_interval': 10, 'auth': { 'allow-anonymous': True, 'password-file': os.path.join(os.path.dirname(os.path.realpath(__file__)), "passwd"), 'plugins': [ 'auth_file', 'auth_anonymous' ] }, 'topic-check': { 'enabled': False } } logger = logging.getLogger(__name__) def main(*args, **kwargs): if sys.version_info[:2] < (3, 4): logger.fatal("Error: Python 3.4+ is required") sys.exit(-1) arguments = docopt(__doc__, version=get_version()) formatter = "[%(asctime)s] :: %(levelname)s - %(message)s" if arguments['-d']: level = logging.DEBUG else: level = logging.INFO logging.basicConfig(level=level, format=formatter) config = None if arguments['-c']: config = read_yaml_config(arguments['-c']) else: config = read_yaml_config(os.path.join(os.path.dirname(os.path.realpath(__file__)), 'default_broker.yaml')) logger.debug("Using default configuration") loop = asyncio.get_event_loop() broker = Broker(config) try: loop.run_until_complete(broker.start()) loop.run_forever() except KeyboardInterrupt: loop.run_until_complete(broker.shutdown()) finally: loop.close() if __name__ == "__main__": main() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1579961068.0 hbmqtt-0.9.6/scripts/default_broker.yaml0000644000175000017510000000026100000000000020273 0ustar00niconico00000000000000listeners: default: type: tcp bind: 0.0.0.0:1883 sys_interval: 20 auth: allow-anonymous: true plugins: - auth_file - auth_anonymous topic-check: enabled: False././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1579961068.0 hbmqtt-0.9.6/scripts/default_client.yaml0000644000175000017510000000021000000000000020257 0ustar00niconico00000000000000keep_alive: 10 ping_delay: 1 default_qos: 0 default_retain: false auto_reconnect: false reconnect_max_interval: 10 reconnect_retries: 2 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1579961068.0 hbmqtt-0.9.6/scripts/pub_script.py0000644000175000017510000001374400000000000017155 0ustar00niconico00000000000000# Copyright (c) 2015 Nicolas JOUANIN # # See the file license.txt for copying permission. """ hbmqtt_pub - MQTT 3.1.1 publisher Usage: hbmqtt_pub --version hbmqtt_pub (-h | --help) hbmqtt_pub --url BROKER_URL -t TOPIC (-f FILE | -l | -m MESSAGE | -n | -s) [-c CONFIG_FILE] [-i CLIENT_ID] [-q | --qos QOS] [-d] [-k KEEP_ALIVE] [--clean-session] [--ca-file CAFILE] [--ca-path CAPATH] [--ca-data CADATA] [ --will-topic WILL_TOPIC [--will-message WILL_MESSAGE] [--will-qos WILL_QOS] [--will-retain] ] [--extra-headers HEADER] [-r] Options: -h --help Show this screen. --version Show version. --url BROKER_URL Broker connection URL (musr conform to MQTT URI scheme (see https://github.com/mqtt/mqtt.github.io/wiki/URI-Scheme>) -c CONFIG_FILE Broker configuration file (YAML format) -i CLIENT_ID Id to use as client ID. -q | --qos QOS Quality of service to use for the message, from 0, 1 and 2. Defaults to 0. -r Set retain flag on connect -t TOPIC Message topic -m MESSAGE Message data to send -f FILE Read file by line and publish message for each line -s Read from stdin and publish message for each line -k KEEP_ALIVE Keep alive timeout in second --clean-session Clean session on connect (defaults to False) --ca-file CAFILE] CA file --ca-path CAPATH] CA Path --ca-data CADATA CA data --will-topic WILL_TOPIC --will-message WILL_MESSAGE --will-qos WILL_QOS --will-retain --extra-headers EXTRA_HEADERS JSON object with key-value pairs of additional headers for websocket connections -d Enable debug messages """ import sys import logging import asyncio import os import json from hbmqtt.client import MQTTClient, ConnectException from hbmqtt.version import get_version from docopt import docopt from hbmqtt.utils import read_yaml_config logger = logging.getLogger(__name__) def _gen_client_id(): import os import socket pid = os.getpid() hostname = socket.gethostname() return "hbmqtt_pub/%d-%s" % (pid, hostname) def _get_qos(arguments): try: return int(arguments['--qos'][0]) except: return None def _get_extra_headers(arguments): try: return json.loads(arguments['--extra-headers']) except: return {} def _get_message(arguments): if arguments['-n']: yield b'' if arguments['-m']: yield arguments['-m'].encode(encoding='utf-8') if arguments['-f']: try: with open(arguments['-f'], 'r') as f: for line in f: yield line.encode(encoding='utf-8') except: logger.error("Failed to read file '%s'" % arguments['-f']) if arguments['-l']: import sys for line in sys.stdin: if line: yield line.encode(encoding='utf-8') if arguments['-s']: import sys message = bytearray() for line in sys.stdin: message.extend(line.encode(encoding='utf-8')) yield message @asyncio.coroutine def do_pub(client, arguments): running_tasks = [] try: logger.info("%s Connecting to broker" % client.client_id) yield from client.connect(uri=arguments['--url'], cleansession=arguments['--clean-session'], cafile=arguments['--ca-file'], capath=arguments['--ca-path'], cadata=arguments['--ca-data'], extra_headers=_get_extra_headers(arguments)) qos = _get_qos(arguments) topic = arguments['-t'] retain = arguments['-r'] for message in _get_message(arguments): logger.info("%s Publishing to '%s'" % (client.client_id, topic)) task = asyncio.ensure_future(client.publish(topic, message, qos, retain)) running_tasks.append(task) if running_tasks: yield from asyncio.wait(running_tasks) yield from client.disconnect() logger.info("%s Disconnected from broker" % client.client_id) except KeyboardInterrupt: yield from client.disconnect() logger.info("%s Disconnected from broker" % client.client_id) except ConnectException as ce: logger.fatal("connection to '%s' failed: %r" % (arguments['--url'], ce)) except asyncio.CancelledError as cae: logger.fatal("Publish canceled due to prvious error") def main(*args, **kwargs): if sys.version_info[:2] < (3, 4): logger.fatal("Error: Python 3.4+ is required") sys.exit(-1) arguments = docopt(__doc__, version=get_version()) #print(arguments) formatter = "[%(asctime)s] :: %(levelname)s - %(message)s" if arguments['-d']: level = logging.DEBUG else: level = logging.INFO logging.basicConfig(level=level, format=formatter) config = None if arguments['-c']: config = read_yaml_config(arguments['-c']) else: config = read_yaml_config(os.path.join(os.path.dirname(os.path.realpath(__file__)), 'default_client.yaml')) logger.debug("Using default configuration") loop = asyncio.get_event_loop() client_id = arguments.get("-i", None) if not client_id: client_id = _gen_client_id() if arguments['-k']: config['keep_alive'] = int(arguments['-k']) if arguments['--will-topic'] and arguments['--will-message'] and arguments['--will-qos']: config['will'] = dict() config['will']['topic'] = arguments['--will-topic'] config['will']['message'] = arguments['--will-message'].encode('utf-8') config['will']['qos'] = int(arguments['--will-qos']) config['will']['retain'] = arguments['--will-retain'] client = MQTTClient(client_id=client_id, config=config, loop=loop) loop.run_until_complete(do_pub(client, arguments)) loop.close() if __name__ == "__main__": main() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1579961068.0 hbmqtt-0.9.6/scripts/sub_script.py0000644000175000017510000001244400000000000017154 0ustar00niconico00000000000000# Copyright (c) 2015 Nicolas JOUANIN # # See the file license.txt for copying permission. """ hbmqtt_sub - MQTT 3.1.1 publisher Usage: hbmqtt_sub --version hbmqtt_sub (-h | --help) hbmqtt_sub --url BROKER_URL -t TOPIC... [-n COUNT] [-c CONFIG_FILE] [-i CLIENT_ID] [-q | --qos QOS] [-d] [-k KEEP_ALIVE] [--clean-session] [--ca-file CAFILE] [--ca-path CAPATH] [--ca-data CADATA] [ --will-topic WILL_TOPIC [--will-message WILL_MESSAGE] [--will-qos WILL_QOS] [--will-retain] ] [--extra-headers HEADER] Options: -h --help Show this screen. --version Show version. --url BROKER_URL Broker connection URL (musr conform to MQTT URI scheme (see https://github.com/mqtt/mqtt.github.io/wiki/URI-Scheme>) -c CONFIG_FILE Broker configuration file (YAML format) -i CLIENT_ID Id to use as client ID. -n COUNT Number of messages to read before ending. -q | --qos QOS Quality of service desired to receive messages, from 0, 1 and 2. Defaults to 0. -t TOPIC... Topic filter to subcribe -k KEEP_ALIVE Keep alive timeout in second --clean-session Clean session on connect (defaults to False) --ca-file CAFILE] CA file --ca-path CAPATH] CA Path --ca-data CADATA CA data --will-topic WILL_TOPIC --will-message WILL_MESSAGE --will-qos WILL_QOS --will-retain --extra-headers EXTRA_HEADERS JSON object with key-value pairs of additional headers for websocket connections -d Enable debug messages """ import sys import logging import asyncio import os import json from hbmqtt.client import MQTTClient, ConnectException from hbmqtt.errors import MQTTException from hbmqtt.version import get_version from docopt import docopt from hbmqtt.mqtt.constants import QOS_0 from hbmqtt.utils import read_yaml_config logger = logging.getLogger(__name__) def _gen_client_id(): import os import socket pid = os.getpid() hostname = socket.gethostname() return "hbmqtt_sub/%d-%s" % (pid, hostname) def _get_qos(arguments): try: return int(arguments['--qos'][0]) except: return QOS_0 def _get_extra_headers(arguments): try: return json.loads(arguments['--extra-headers']) except: return {} @asyncio.coroutine def do_sub(client, arguments): try: yield from client.connect(uri=arguments['--url'], cleansession=arguments['--clean-session'], cafile=arguments['--ca-file'], capath=arguments['--ca-path'], cadata=arguments['--ca-data'], extra_headers=_get_extra_headers(arguments)) qos = _get_qos(arguments) filters = [] for topic in arguments['-t']: filters.append((topic, qos)) yield from client.subscribe(filters) if arguments['-n']: max_count = int(arguments['-n']) else: max_count = None count = 0 while True: if max_count and count >= max_count: break try: message = yield from client.deliver_message() count += 1 sys.stdout.buffer.write(message.publish_packet.data) sys.stdout.write('\n') except MQTTException: logger.debug("Error reading packet") yield from client.disconnect() except KeyboardInterrupt: yield from client.disconnect() except ConnectException as ce: logger.fatal("connection to '%s' failed: %r" % (arguments['--url'], ce)) except asyncio.CancelledError as cae: logger.fatal("Publish canceled due to prvious error") def main(*args, **kwargs): if sys.version_info[:2] < (3, 4): logger.fatal("Error: Python 3.4+ is required") sys.exit(-1) arguments = docopt(__doc__, version=get_version()) #print(arguments) formatter = "[%(asctime)s] :: %(levelname)s - %(message)s" if arguments['-d']: level = logging.DEBUG else: level = logging.INFO logging.basicConfig(level=level, format=formatter) config = None if arguments['-c']: config = read_yaml_config(arguments['-c']) else: config = read_yaml_config(os.path.join(os.path.dirname(os.path.realpath(__file__)), 'default_client.yaml')) logger.debug(os.path.join(os.path.dirname(os.path.realpath(__file__)), 'default_client.yaml')) logger.debug("Using default configuration") loop = asyncio.get_event_loop() client_id = arguments.get("-i", None) if not client_id: client_id = _gen_client_id() if arguments['-k']: config['keep_alive'] = int(arguments['-k']) if arguments['--will-topic'] and arguments['--will-message'] and arguments['--will-qos']: config['will'] = dict() config['will']['topic'] = arguments['--will-topic'] config['will']['message'] = arguments['--will-message'].encode('utf-8') config['will']['qos'] = int(arguments['--will-qos']) config['will']['retain'] = arguments['--will-retain'] client = MQTTClient(client_id=client_id, config=config, loop=loop) loop.run_until_complete(do_sub(client, arguments)) loop.close() if __name__ == "__main__": main() ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1579961563.696805 hbmqtt-0.9.6/setup.cfg0000644000175000017510000000011400000000000014546 0ustar00niconico00000000000000[bdist_wheel] python-tag = py34.py35 [egg_info] tag_build = tag_date = 0 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1579961068.0 hbmqtt-0.9.6/setup.py0000644000175000017510000000447600000000000014456 0ustar00niconico00000000000000# Copyright (c) 2015 Nicolas JOUANIN # # See the file license.txt for copying permission. from setuptools import setup, find_packages from hbmqtt.version import get_version setup( name="hbmqtt", version=get_version(), description="MQTT client/broker using Python 3.4 asyncio library", author="Nicolas Jouanin", author_email='nico@beerfactory.org', url="https://github.com/beerfactory/hbmqtt", license='MIT', packages=find_packages(exclude=['tests']), include_package_data=True, platforms='all', install_requires=[ 'transitions', 'websockets', 'passlib', 'docopt', 'pyyaml' ], classifiers=[ 'Development Status :: 3 - Alpha', 'Intended Audience :: Developers', 'License :: OSI Approved :: MIT License', 'Operating System :: POSIX', 'Operating System :: MacOS', 'Operating System :: Microsoft :: Windows', 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', 'Topic :: Communications', 'Topic :: Internet' ], entry_points={ 'hbmqtt.test.plugins': [ 'test_plugin = tests.plugins.test_manager:TestPlugin', 'event_plugin = tests.plugins.test_manager:EventTestPlugin', 'packet_logger_plugin = hbmqtt.plugins.logging:PacketLoggerPlugin', ], 'hbmqtt.broker.plugins': [ # 'event_logger_plugin = hbmqtt.plugins.logging:EventLoggerPlugin', 'packet_logger_plugin = hbmqtt.plugins.logging:PacketLoggerPlugin', 'auth_anonymous = hbmqtt.plugins.authentication:AnonymousAuthPlugin', 'auth_file = hbmqtt.plugins.authentication:FileAuthPlugin', 'topic_taboo = hbmqtt.plugins.topic_checking:TopicTabooPlugin', 'topic_acl = hbmqtt.plugins.topic_checking:TopicAccessControlListPlugin', 'broker_sys = hbmqtt.plugins.sys.broker:BrokerSysPlugin', ], 'hbmqtt.client.plugins': [ 'packet_logger_plugin = hbmqtt.plugins.logging:PacketLoggerPlugin', ], 'console_scripts': [ 'hbmqtt = scripts.broker_script:main', 'hbmqtt_pub = scripts.pub_script:main', 'hbmqtt_sub = scripts.sub_script:main', ] } ) ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1579961563.696805 hbmqtt-0.9.6/tests/0000755000175000017510000000000000000000000014073 5ustar00niconico00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1579961068.0 hbmqtt-0.9.6/tests/__init__.py0000644000175000017510000000002400000000000016200 0ustar00niconico00000000000000__author__ = 'nico' ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1579961068.0 hbmqtt-0.9.6/tests/mosquitto.org.crt0000644000175000017510000000206600000000000017443 0ustar00niconico00000000000000-----BEGIN CERTIFICATE----- MIIC8DCCAlmgAwIBAgIJAOD63PlXjJi8MA0GCSqGSIb3DQEBBQUAMIGQMQswCQYD VQQGEwJHQjEXMBUGA1UECAwOVW5pdGVkIEtpbmdkb20xDjAMBgNVBAcMBURlcmJ5 MRIwEAYDVQQKDAlNb3NxdWl0dG8xCzAJBgNVBAsMAkNBMRYwFAYDVQQDDA1tb3Nx dWl0dG8ub3JnMR8wHQYJKoZIhvcNAQkBFhByb2dlckBhdGNob28ub3JnMB4XDTEy MDYyOTIyMTE1OVoXDTIyMDYyNzIyMTE1OVowgZAxCzAJBgNVBAYTAkdCMRcwFQYD VQQIDA5Vbml0ZWQgS2luZ2RvbTEOMAwGA1UEBwwFRGVyYnkxEjAQBgNVBAoMCU1v c3F1aXR0bzELMAkGA1UECwwCQ0ExFjAUBgNVBAMMDW1vc3F1aXR0by5vcmcxHzAd BgkqhkiG9w0BCQEWEHJvZ2VyQGF0Y2hvby5vcmcwgZ8wDQYJKoZIhvcNAQEBBQAD gY0AMIGJAoGBAMYkLmX7SqOT/jJCZoQ1NWdCrr/pq47m3xxyXcI+FLEmwbE3R9vM rE6sRbP2S89pfrCt7iuITXPKycpUcIU0mtcT1OqxGBV2lb6RaOT2gC5pxyGaFJ+h A+GIbdYKO3JprPxSBoRponZJvDGEZuM3N7p3S/lRoi7G5wG5mvUmaE5RAgMBAAGj UDBOMB0GA1UdDgQWBBTad2QneVztIPQzRRGj6ZHKqJTv5jAfBgNVHSMEGDAWgBTa d2QneVztIPQzRRGj6ZHKqJTv5jAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBQUA A4GBAAqw1rK4NlRUCUBLhEFUQasjP7xfFqlVbE2cRy0Rs4o3KS0JwzQVBwG85xge REyPOFdGdhBY2P1FNRy0MDr6xr+D2ZOwxs63dG1nnAnWZg7qwoLgpZ4fESPD3PkA 1ZgKJc2zbSQ9fCPxt2W3mdVav66c6fsb7els2W2Iz7gERJSX -----END CERTIFICATE----- ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1579961563.696805 hbmqtt-0.9.6/tests/mqtt/0000755000175000017510000000000000000000000015060 5ustar00niconico00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1579961068.0 hbmqtt-0.9.6/tests/mqtt/__init__.py0000644000175000017510000000002400000000000017165 0ustar00niconico00000000000000__author__ = 'nico' ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1579961563.696805 hbmqtt-0.9.6/tests/mqtt/protocol/0000755000175000017510000000000000000000000016721 5ustar00niconico00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1579961068.0 hbmqtt-0.9.6/tests/mqtt/protocol/__init__.py0000644000175000017510000000002400000000000021026 0ustar00niconico00000000000000__author__ = 'nico' ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1579961068.0 hbmqtt-0.9.6/tests/mqtt/protocol/test_handler.py0000644000175000017510000005156400000000000021762 0ustar00niconico00000000000000# Copyright (c) 2015 Nicolas JOUANIN # # See the file license.txt for copying permission. import unittest import asyncio import logging import random from hbmqtt.plugins.manager import PluginManager from hbmqtt.session import Session, OutgoingApplicationMessage, IncomingApplicationMessage from hbmqtt.mqtt.protocol.handler import ProtocolHandler from hbmqtt.adapters import StreamWriterAdapter, StreamReaderAdapter from hbmqtt.mqtt.constants import QOS_0, QOS_1, QOS_2 from hbmqtt.mqtt.publish import PublishPacket from hbmqtt.mqtt.puback import PubackPacket from hbmqtt.mqtt.pubrec import PubrecPacket from hbmqtt.mqtt.pubrel import PubrelPacket from hbmqtt.mqtt.pubcomp import PubcompPacket formatter = "[%(asctime)s] %(name)s {%(filename)s:%(lineno)d} %(levelname)s - %(message)s" logging.basicConfig(level=logging.DEBUG, format=formatter) log = logging.getLogger(__name__) def rand_packet_id(): return random.randint(0, 65535) def adapt(reader, writer): return StreamReaderAdapter(reader), StreamWriterAdapter(writer) class ProtocolHandlerTest(unittest.TestCase): def setUp(self): self.loop = asyncio.new_event_loop() asyncio.set_event_loop(self.loop) self.plugin_manager = PluginManager("hbmqtt.test.plugins", context=None, loop=self.loop) def tearDown(self): self.loop.close() def test_init_handler(self): Session() handler = ProtocolHandler(self.plugin_manager, loop=self.loop) self.assertIsNone(handler.session) self.assertIs(handler._loop, self.loop) self.check_empty_waiters(handler) def test_start_stop(self): @asyncio.coroutine def server_mock(reader, writer): pass @asyncio.coroutine def test_coro(): try: s = Session() reader, writer = yield from asyncio.open_connection('127.0.0.1', 8888) reader_adapted, writer_adapted = adapt(reader, writer) handler = ProtocolHandler(self.plugin_manager) handler.attach(s, reader_adapted, writer_adapted) yield from self.start_handler(handler, s) yield from self.stop_handler(handler, s) future.set_result(True) except Exception as ae: future.set_exception(ae) future = asyncio.Future(loop=self.loop) coro = asyncio.start_server(server_mock, '127.0.0.1', 8888) server = self.loop.run_until_complete(coro) self.loop.run_until_complete(test_coro()) server.close() self.loop.run_until_complete(server.wait_closed()) if future.exception(): raise future.exception() def test_publish_qos0(self): @asyncio.coroutine def server_mock(reader, writer): try: packet = yield from PublishPacket.from_stream(reader) self.assertEqual(packet.variable_header.topic_name, '/topic') self.assertEqual(packet.qos, QOS_0) self.assertIsNone(packet.packet_id) except Exception as ae: future.set_exception(ae) @asyncio.coroutine def test_coro(): try: s = Session() reader, writer = yield from asyncio.open_connection('127.0.0.1', 8888, loop=self.loop) reader_adapted, writer_adapted = adapt(reader, writer) handler = ProtocolHandler(self.plugin_manager, loop=self.loop) handler.attach(s, reader_adapted, writer_adapted) yield from self.start_handler(handler, s) message = yield from handler.mqtt_publish('/topic', b'test_data', QOS_0, False) self.assertIsInstance(message, OutgoingApplicationMessage) self.assertIsNotNone(message.publish_packet) self.assertIsNone(message.puback_packet) self.assertIsNone(message.pubrec_packet) self.assertIsNone(message.pubrel_packet) self.assertIsNone(message.pubcomp_packet) yield from self.stop_handler(handler, s) future.set_result(True) except Exception as ae: future.set_exception(ae) future = asyncio.Future(loop=self.loop) coro = asyncio.start_server(server_mock, '127.0.0.1', 8888, loop=self.loop) server = self.loop.run_until_complete(coro) self.loop.run_until_complete(test_coro()) server.close() self.loop.run_until_complete(server.wait_closed()) if future.exception(): raise future.exception() def test_publish_qos1(self): @asyncio.coroutine def server_mock(reader, writer): packet = yield from PublishPacket.from_stream(reader) try: self.assertEqual(packet.variable_header.topic_name, '/topic') self.assertEqual(packet.qos, QOS_1) self.assertIsNotNone(packet.packet_id) self.assertIn(packet.packet_id, self.session.inflight_out) self.assertIn(packet.packet_id, self.handler._puback_waiters) puback = PubackPacket.build(packet.packet_id) yield from puback.to_stream(writer) except Exception as ae: future.set_exception(ae) @asyncio.coroutine def test_coro(): try: reader, writer = yield from asyncio.open_connection('127.0.0.1', 8888, loop=self.loop) reader_adapted, writer_adapted = adapt(reader, writer) self.handler = ProtocolHandler(self.plugin_manager, loop=self.loop) self.handler.attach(self.session, reader_adapted, writer_adapted) yield from self.start_handler(self.handler, self.session) message = yield from self.handler.mqtt_publish('/topic', b'test_data', QOS_1, False) self.assertIsInstance(message, OutgoingApplicationMessage) self.assertIsNotNone(message.publish_packet) self.assertIsNotNone(message.puback_packet) self.assertIsNone(message.pubrec_packet) self.assertIsNone(message.pubrel_packet) self.assertIsNone(message.pubcomp_packet) yield from self.stop_handler(self.handler, self.session) if not future.done(): future.set_result(True) except Exception as ae: future.set_exception(ae) self.handler = None self.session = Session() future = asyncio.Future(loop=self.loop) coro = asyncio.start_server(server_mock, '127.0.0.1', 8888, loop=self.loop) server = self.loop.run_until_complete(coro) self.loop.run_until_complete(test_coro()) server.close() self.loop.run_until_complete(server.wait_closed()) if future.exception(): raise future.exception() def test_publish_qos2(self): @asyncio.coroutine def server_mock(reader, writer): try: packet = yield from PublishPacket.from_stream(reader) self.assertEqual(packet.topic_name, '/topic') self.assertEqual(packet.qos, QOS_2) self.assertIsNotNone(packet.packet_id) self.assertIn(packet.packet_id, self.session.inflight_out) self.assertIn(packet.packet_id, self.handler._pubrec_waiters) pubrec = PubrecPacket.build(packet.packet_id) yield from pubrec.to_stream(writer) yield from PubrelPacket.from_stream(reader) self.assertIn(packet.packet_id, self.handler._pubcomp_waiters) pubcomp = PubcompPacket.build(packet.packet_id) yield from pubcomp.to_stream(writer) except Exception as ae: future.set_exception(ae) @asyncio.coroutine def test_coro(): try: reader, writer = yield from asyncio.open_connection('127.0.0.1', 8888, loop=self.loop) reader_adapted, writer_adapted = adapt(reader, writer) self.handler = ProtocolHandler(self.plugin_manager, loop=self.loop) self.handler.attach(self.session, reader_adapted, writer_adapted) yield from self.start_handler(self.handler, self.session) message = yield from self.handler.mqtt_publish('/topic', b'test_data', QOS_2, False) self.assertIsInstance(message, OutgoingApplicationMessage) self.assertIsNotNone(message.publish_packet) self.assertIsNone(message.puback_packet) self.assertIsNotNone(message.pubrec_packet) self.assertIsNotNone(message.pubrel_packet) self.assertIsNotNone(message.pubcomp_packet) yield from self.stop_handler(self.handler, self.session) if not future.done(): future.set_result(True) except Exception as ae: future.set_exception(ae) self.handler = None self.session = Session() future = asyncio.Future(loop=self.loop) coro = asyncio.start_server(server_mock, '127.0.0.1', 8888, loop=self.loop) server = self.loop.run_until_complete(coro) self.loop.run_until_complete(test_coro()) server.close() self.loop.run_until_complete(server.wait_closed()) if future.exception(): raise future.exception() def test_receive_qos0(self): @asyncio.coroutine def server_mock(reader, writer): packet = PublishPacket.build('/topic', b'test_data', rand_packet_id(), False, QOS_0, False) yield from packet.to_stream(writer) @asyncio.coroutine def test_coro(): try: reader, writer = yield from asyncio.open_connection('127.0.0.1', 8888, loop=self.loop) reader_adapted, writer_adapted = adapt(reader, writer) self.handler = ProtocolHandler(self.plugin_manager, loop=self.loop) self.handler.attach(self.session, reader_adapted, writer_adapted) yield from self.start_handler(self.handler, self.session) message = yield from self.handler.mqtt_deliver_next_message() self.assertIsInstance(message, IncomingApplicationMessage) self.assertIsNotNone(message.publish_packet) self.assertIsNone(message.puback_packet) self.assertIsNone(message.pubrec_packet) self.assertIsNone(message.pubrel_packet) self.assertIsNone(message.pubcomp_packet) yield from self.stop_handler(self.handler, self.session) future.set_result(True) except Exception as ae: future.set_exception(ae) self.handler = None self.session = Session() future = asyncio.Future(loop=self.loop) coro = asyncio.start_server(server_mock, '127.0.0.1', 8888, loop=self.loop) server = self.loop.run_until_complete(coro) self.loop.run_until_complete(test_coro()) server.close() self.loop.run_until_complete(server.wait_closed()) if future.exception(): raise future.exception() def test_receive_qos1(self): @asyncio.coroutine def server_mock(reader, writer): try: packet = PublishPacket.build('/topic', b'test_data', rand_packet_id(), False, QOS_1, False) yield from packet.to_stream(writer) puback = yield from PubackPacket.from_stream(reader) self.assertIsNotNone(puback) self.assertEqual(packet.packet_id, puback.packet_id) except Exception as ae: print(ae) future.set_exception(ae) @asyncio.coroutine def test_coro(): try: reader, writer = yield from asyncio.open_connection('127.0.0.1', 8888, loop=self.loop) reader_adapted, writer_adapted = adapt(reader, writer) self.handler = ProtocolHandler(self.plugin_manager, loop=self.loop) self.handler.attach(self.session, reader_adapted, writer_adapted) yield from self.start_handler(self.handler, self.session) message = yield from self.handler.mqtt_deliver_next_message() self.assertIsInstance(message, IncomingApplicationMessage) self.assertIsNotNone(message.publish_packet) self.assertIsNotNone(message.puback_packet) self.assertIsNone(message.pubrec_packet) self.assertIsNone(message.pubrel_packet) self.assertIsNone(message.pubcomp_packet) yield from self.stop_handler(self.handler, self.session) future.set_result(True) except Exception as ae: future.set_exception(ae) self.handler = None self.session = Session() future = asyncio.Future(loop=self.loop) self.event = asyncio.Event(loop=self.loop) coro = asyncio.start_server(server_mock, '127.0.0.1', 8888, loop=self.loop) server = self.loop.run_until_complete(coro) self.loop.run_until_complete(test_coro()) server.close() self.loop.run_until_complete(server.wait_closed()) if future.exception(): raise future.exception() def test_receive_qos2(self): @asyncio.coroutine def server_mock(reader, writer): try: packet = PublishPacket.build('/topic', b'test_data', rand_packet_id(), False, QOS_2, False) yield from packet.to_stream(writer) pubrec = yield from PubrecPacket.from_stream(reader) self.assertIsNotNone(pubrec) self.assertEqual(packet.packet_id, pubrec.packet_id) self.assertIn(packet.packet_id, self.handler._pubrel_waiters) pubrel = PubrelPacket.build(packet.packet_id) yield from pubrel.to_stream(writer) pubcomp = yield from PubcompPacket.from_stream(reader) self.assertIsNotNone(pubcomp) self.assertEqual(packet.packet_id, pubcomp.packet_id) except Exception as ae: future.set_exception(ae) @asyncio.coroutine def test_coro(): try: reader, writer = yield from asyncio.open_connection('127.0.0.1', 8888, loop=self.loop) reader_adapted, writer_adapted = adapt(reader, writer) self.handler = ProtocolHandler(self.plugin_manager, loop=self.loop) self.handler.attach(self.session, reader_adapted, writer_adapted) yield from self.start_handler(self.handler, self.session) message = yield from self.handler.mqtt_deliver_next_message() self.assertIsInstance(message, IncomingApplicationMessage) self.assertIsNotNone(message.publish_packet) self.assertIsNone(message.puback_packet) self.assertIsNotNone(message.pubrec_packet) self.assertIsNotNone(message.pubrel_packet) self.assertIsNotNone(message.pubcomp_packet) yield from self.stop_handler(self.handler, self.session) future.set_result(True) except Exception as ae: future.set_exception(ae) self.handler = None self.session = Session() future = asyncio.Future(loop=self.loop) coro = asyncio.start_server(server_mock, '127.0.0.1', 8888, loop=self.loop) server = self.loop.run_until_complete(coro) self.loop.run_until_complete(test_coro()) server.close() self.loop.run_until_complete(server.wait_closed()) if future.exception(): raise future.exception() @asyncio.coroutine def start_handler(self, handler, session): self.check_empty_waiters(handler) self.check_no_message(session) yield from handler.start() self.assertTrue(handler._reader_ready) @asyncio.coroutine def stop_handler(self, handler, session): yield from handler.stop() self.assertTrue(handler._reader_stopped) self.check_empty_waiters(handler) self.check_no_message(session) def check_empty_waiters(self, handler): self.assertFalse(handler._puback_waiters) self.assertFalse(handler._pubrec_waiters) self.assertFalse(handler._pubrel_waiters) self.assertFalse(handler._pubcomp_waiters) def check_no_message(self, session): self.assertFalse(session.inflight_out) self.assertFalse(session.inflight_in) def test_publish_qos1_retry(self): @asyncio.coroutine def server_mock(reader, writer): packet = yield from PublishPacket.from_stream(reader) try: self.assertEqual(packet.topic_name, '/topic') self.assertEqual(packet.qos, QOS_1) self.assertIsNotNone(packet.packet_id) self.assertIn(packet.packet_id, self.session.inflight_out) self.assertIn(packet.packet_id, self.handler._puback_waiters) puback = PubackPacket.build(packet.packet_id) yield from puback.to_stream(writer) except Exception as ae: future.set_exception(ae) @asyncio.coroutine def test_coro(): try: reader, writer = yield from asyncio.open_connection('127.0.0.1', 8888, loop=self.loop) reader_adapted, writer_adapted = adapt(reader, writer) self.handler = ProtocolHandler(self.plugin_manager, loop=self.loop) self.handler.attach(self.session, reader_adapted, writer_adapted) yield from self.handler.start() yield from self.stop_handler(self.handler, self.session) if not future.done(): future.set_result(True) except Exception as ae: future.set_exception(ae) self.handler = None self.session = Session() message = OutgoingApplicationMessage(1, '/topic', QOS_1, b'test_data', False) message.publish_packet = PublishPacket.build('/topic', b'test_data', rand_packet_id(), False, QOS_1, False) self.session.inflight_out[1] = message future = asyncio.Future(loop=self.loop) coro = asyncio.start_server(server_mock, '127.0.0.1', 8888, loop=self.loop) server = self.loop.run_until_complete(coro) self.loop.run_until_complete(test_coro()) server.close() self.loop.run_until_complete(server.wait_closed()) if future.exception(): raise future.exception() def test_publish_qos2_retry(self): @asyncio.coroutine def server_mock(reader, writer): try: packet = yield from PublishPacket.from_stream(reader) self.assertEqual(packet.topic_name, '/topic') self.assertEqual(packet.qos, QOS_2) self.assertIsNotNone(packet.packet_id) self.assertIn(packet.packet_id, self.session.inflight_out) self.assertIn(packet.packet_id, self.handler._pubrec_waiters) pubrec = PubrecPacket.build(packet.packet_id) yield from pubrec.to_stream(writer) yield from PubrelPacket.from_stream(reader) self.assertIn(packet.packet_id, self.handler._pubcomp_waiters) pubcomp = PubcompPacket.build(packet.packet_id) yield from pubcomp.to_stream(writer) except Exception as ae: future.set_exception(ae) @asyncio.coroutine def test_coro(): try: reader, writer = yield from asyncio.open_connection('127.0.0.1', 8888, loop=self.loop) reader_adapted, writer_adapted = adapt(reader, writer) self.handler = ProtocolHandler(self.plugin_manager, loop=self.loop) self.handler.attach(self.session, reader_adapted, writer_adapted) yield from self.handler.start() yield from self.stop_handler(self.handler, self.session) if not future.done(): future.set_result(True) except Exception as ae: future.set_exception(ae) self.handler = None self.session = Session() message = OutgoingApplicationMessage(1, '/topic', QOS_2, b'test_data', False) message.publish_packet = PublishPacket.build('/topic', b'test_data', rand_packet_id(), False, QOS_2, False) self.session.inflight_out[1] = message future = asyncio.Future(loop=self.loop) coro = asyncio.start_server(server_mock, '127.0.0.1', 8888, loop=self.loop) server = self.loop.run_until_complete(coro) self.loop.run_until_complete(test_coro()) server.close() self.loop.run_until_complete(server.wait_closed()) if future.exception(): raise future.exception() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1579961068.0 hbmqtt-0.9.6/tests/mqtt/test_connect.py0000644000175000017510000001517200000000000020130 0ustar00niconico00000000000000# Copyright (c) 2015 Nicolas JOUANIN # # See the file license.txt for copying permission. import unittest import asyncio from hbmqtt.mqtt.connect import ConnectPacket, ConnectVariableHeader, ConnectPayload from hbmqtt.mqtt.packet import MQTTFixedHeader, CONNECT from hbmqtt.adapters import BufferReader class ConnectPacketTest(unittest.TestCase): def setUp(self): self.loop = asyncio.new_event_loop() def test_decode_ok(self): data = b'\x10\x3e\x00\x04MQTT\x04\xce\x00\x00\x00\x0a0123456789\x00\x09WillTopic\x00\x0bWillMessage\x00\x04user\x00\x08password' stream = BufferReader(data) message = self.loop.run_until_complete(ConnectPacket.from_stream(stream)) self.assertEqual(message.variable_header.proto_name, "MQTT") self.assertEqual(message.variable_header.proto_level, 4) self.assertTrue(message.variable_header.username_flag) self.assertTrue(message.variable_header.password_flag) self.assertFalse(message.variable_header.will_retain_flag) self.assertEqual(message.variable_header.will_qos, 1) self.assertTrue(message.variable_header.will_flag) self.assertTrue(message.variable_header.clean_session_flag) self.assertFalse(message.variable_header.reserved_flag) self.assertEqual(message.payload.client_id, '0123456789') self.assertEqual(message.payload.will_topic, 'WillTopic') self.assertEqual(message.payload.will_message, b'WillMessage') self.assertEqual(message.payload.username, 'user') self.assertEqual(message.payload.password, 'password') def test_decode_ok_will_flag(self): data = b'\x10\x26\x00\x04MQTT\x04\xca\x00\x00\x00\x0a0123456789\x00\x04user\x00\x08password' stream = BufferReader(data) message = self.loop.run_until_complete(ConnectPacket.from_stream(stream)) self.assertEqual(message.variable_header.proto_name, "MQTT") self.assertEqual(message.variable_header.proto_level, 4) self.assertTrue(message.variable_header.username_flag) self.assertTrue(message.variable_header.password_flag) self.assertFalse(message.variable_header.will_retain_flag) self.assertEqual(message.variable_header.will_qos, 1) self.assertFalse(message.variable_header.will_flag) self.assertTrue(message.variable_header.clean_session_flag) self.assertFalse(message.variable_header.reserved_flag) self.assertEqual(message.payload.client_id, '0123456789') self.assertEqual(message.payload.will_topic, None) self.assertEqual(message.payload.will_message, None) self.assertEqual(message.payload.username, 'user') self.assertEqual(message.payload.password, 'password') def test_decode_fail_reserved_flag(self): data = b'\x10\x3e\x00\x04MQTT\x04\xcf\x00\x00\x00\x0a0123456789\x00\x09WillTopic\x00\x0bWillMessage\x00\x04user\x00\x08password' stream = BufferReader(data) message = self.loop.run_until_complete(ConnectPacket.from_stream(stream)) self.assertTrue(message.variable_header.reserved_flag) def test_decode_fail_miss_clientId(self): data = b'\x10\x0a\x00\x04MQTT\x04\xce\x00\x00' stream = BufferReader(data) message = self.loop.run_until_complete(ConnectPacket.from_stream(stream)) self.assertIsNot(message.payload.client_id, None) def test_decode_fail_miss_willtopic(self): data = b'\x10\x16\x00\x04MQTT\x04\xce\x00\x00\x00\x0a0123456789' stream = BufferReader(data) message = self.loop.run_until_complete(ConnectPacket.from_stream(stream)) self.assertIs(message.payload.will_topic, None) def test_decode_fail_miss_username(self): data = b'\x10\x2e\x00\x04MQTT\x04\xce\x00\x00\x00\x0a0123456789\x00\x09WillTopic\x00\x0bWillMessage' stream = BufferReader(data) message = self.loop.run_until_complete(ConnectPacket.from_stream(stream)) self.assertIs(message.payload.username, None) def test_decode_fail_miss_password(self): data = b'\x10\x34\x00\x04MQTT\x04\xce\x00\x00\x00\x0a0123456789\x00\x09WillTopic\x00\x0bWillMessage\x00\x04user' stream = BufferReader(data) message = self.loop.run_until_complete(ConnectPacket.from_stream(stream)) self.assertIs(message.payload.password, None) def test_encode(self): header = MQTTFixedHeader(CONNECT, 0x00, 0) variable_header = ConnectVariableHeader(0xce, 0, 'MQTT', 4) payload = ConnectPayload('0123456789', 'WillTopic', b'WillMessage', 'user', 'password') message = ConnectPacket(header, variable_header, payload) encoded = message.to_bytes() self.assertEqual(encoded, b'\x10\x3e\x00\x04MQTT\x04\xce\x00\x00\x00\x0a0123456789\x00\x09WillTopic\x00\x0bWillMessage\x00\x04user\x00\x08password') def test_getattr_ok(self): data = b'\x10\x3e\x00\x04MQTT\x04\xce\x00\x00\x00\x0a0123456789\x00\x09WillTopic\x00\x0bWillMessage\x00\x04user\x00\x08password' stream = BufferReader(data) message = self.loop.run_until_complete(ConnectPacket.from_stream(stream)) self.assertEqual(message.variable_header.proto_name, "MQTT") self.assertEqual(message.proto_name, "MQTT") self.assertEqual(message.variable_header.proto_level, 4) self.assertEqual(message.proto_level, 4) self.assertTrue(message.variable_header.username_flag) self.assertTrue(message.username_flag) self.assertTrue(message.variable_header.password_flag) self.assertTrue(message.password_flag) self.assertFalse(message.variable_header.will_retain_flag) self.assertFalse(message.will_retain_flag) self.assertEqual(message.variable_header.will_qos, 1) self.assertEqual(message.will_qos, 1) self.assertTrue(message.variable_header.will_flag) self.assertTrue(message.will_flag) self.assertTrue(message.variable_header.clean_session_flag) self.assertTrue(message.clean_session_flag) self.assertFalse(message.variable_header.reserved_flag) self.assertFalse(message.reserved_flag) self.assertEqual(message.payload.client_id, '0123456789') self.assertEqual(message.client_id, '0123456789') self.assertEqual(message.payload.will_topic, 'WillTopic') self.assertEqual(message.will_topic, 'WillTopic') self.assertEqual(message.payload.will_message, b'WillMessage') self.assertEqual(message.will_message, b'WillMessage') self.assertEqual(message.payload.username, 'user') self.assertEqual(message.username, 'user') self.assertEqual(message.payload.password, 'password') self.assertEqual(message.password, 'password') ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1579961068.0 hbmqtt-0.9.6/tests/mqtt/test_packet.py0000644000175000017510000000351200000000000017741 0ustar00niconico00000000000000# Copyright (c) 2015 Nicolas JOUANIN # # See the file license.txt for copying permission. import unittest import asyncio from hbmqtt.mqtt.packet import CONNECT, MQTTFixedHeader from hbmqtt.errors import MQTTException from hbmqtt.adapters import BufferReader class TestMQTTFixedHeaderTest(unittest.TestCase): def setUp(self): self.loop = asyncio.new_event_loop() def test_from_bytes(self): data = b'\x10\x7f' stream = BufferReader(data) header = self.loop.run_until_complete(MQTTFixedHeader.from_stream(stream)) self.assertEqual(header.packet_type, CONNECT) self.assertFalse(header.flags & 0x08) self.assertEqual((header.flags & 0x06) >> 1, 0) self.assertFalse(header.flags & 0x01) self.assertEqual(header.remaining_length, 127) def test_from_bytes_with_length(self): data = b'\x10\xff\xff\xff\x7f' stream = BufferReader(data) header = self.loop.run_until_complete(MQTTFixedHeader.from_stream(stream)) self.assertEqual(header.packet_type, CONNECT) self.assertFalse(header.flags & 0x08) self.assertEqual((header.flags & 0x06) >> 1, 0) self.assertFalse(header.flags & 0x01) self.assertEqual(header.remaining_length, 268435455) def test_from_bytes_ko_with_length(self): data = b'\x10\xff\xff\xff\xff\x7f' stream = BufferReader(data) with self.assertRaises(MQTTException): self.loop.run_until_complete(MQTTFixedHeader.from_stream(stream)) def test_to_bytes(self): header = MQTTFixedHeader(CONNECT, 0x00, 0) data = header.to_bytes() self.assertEqual(data, b'\x10\x00') def test_to_bytes_2(self): header = MQTTFixedHeader(CONNECT, 0x00, 268435455) data = header.to_bytes() self.assertEqual(data, b'\x10\xff\xff\xff\x7f') ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1579961068.0 hbmqtt-0.9.6/tests/mqtt/test_puback.py0000644000175000017510000000146600000000000017745 0ustar00niconico00000000000000# Copyright (c) 2015 Nicolas JOUANIN # # See the file license.txt for copying permission. import asyncio import unittest from hbmqtt.mqtt.puback import PubackPacket, PacketIdVariableHeader from hbmqtt.adapters import BufferReader class PubackPacketTest(unittest.TestCase): def setUp(self): self.loop = asyncio.new_event_loop() def test_from_stream(self): data = b'\x40\x02\x00\x0a' stream = BufferReader(data) message = self.loop.run_until_complete(PubackPacket.from_stream(stream)) self.assertEqual(message.variable_header.packet_id, 10) def test_to_bytes(self): variable_header = PacketIdVariableHeader(10) publish = PubackPacket(variable_header=variable_header) out = publish.to_bytes() self.assertEqual(out, b'\x40\x02\x00\x0a') ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1579961068.0 hbmqtt-0.9.6/tests/mqtt/test_pubcomp.py0000644000175000017510000000147300000000000020143 0ustar00niconico00000000000000# Copyright (c) 2015 Nicolas JOUANIN # # See the file license.txt for copying permission. import asyncio import unittest from hbmqtt.mqtt.pubcomp import PubcompPacket, PacketIdVariableHeader from hbmqtt.adapters import BufferReader class PubcompPacketTest(unittest.TestCase): def setUp(self): self.loop = asyncio.new_event_loop() def test_from_stream(self): data = b'\x70\x02\x00\x0a' stream = BufferReader(data) message = self.loop.run_until_complete(PubcompPacket.from_stream(stream)) self.assertEqual(message.variable_header.packet_id, 10) def test_to_bytes(self): variable_header = PacketIdVariableHeader(10) publish = PubcompPacket(variable_header=variable_header) out = publish.to_bytes() self.assertEqual(out, b'\x70\x02\x00\x0a') ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1579961068.0 hbmqtt-0.9.6/tests/mqtt/test_publish.py0000644000175000017510000001220000000000000020132 0ustar00niconico00000000000000# Copyright (c) 2015 Nicolas JOUANIN # # See the file license.txt for copying permission. import asyncio import unittest from hbmqtt.mqtt.publish import PublishPacket, PublishVariableHeader, PublishPayload from hbmqtt.adapters import BufferReader from hbmqtt.mqtt.constants import QOS_0, QOS_1, QOS_2 class PublishPacketTest(unittest.TestCase): def setUp(self): self.loop = asyncio.new_event_loop() def test_from_stream_qos_0(self): data = b'\x31\x11\x00\x05topic0123456789' stream = BufferReader(data) message = self.loop.run_until_complete(PublishPacket.from_stream(stream)) self.assertEqual(message.variable_header.topic_name, 'topic') self.assertEqual(message.variable_header.packet_id, None) self.assertFalse((message.fixed_header.flags >> 1) & 0x03) self.assertTrue(message.fixed_header.flags & 0x01) self.assertTrue(message.payload.data, b'0123456789') def test_from_stream_qos_2(self): data = b'\x37\x13\x00\x05topic\x00\x0a0123456789' stream = BufferReader(data) message = self.loop.run_until_complete(PublishPacket.from_stream(stream)) self.assertEqual(message.variable_header.topic_name, 'topic') self.assertEqual(message.variable_header.packet_id, 10) self.assertTrue((message.fixed_header.flags >> 1) & 0x03) self.assertTrue(message.fixed_header.flags & 0x01) self.assertTrue(message.payload.data, b'0123456789') def test_to_stream_no_packet_id(self): variable_header = PublishVariableHeader('topic', None) payload = PublishPayload(b'0123456789') publish = PublishPacket(variable_header=variable_header, payload=payload) out = publish.to_bytes() self.assertEqual(out, b'\x30\x11\x00\x05topic0123456789') def test_to_stream_packet(self): variable_header = PublishVariableHeader('topic', 10) payload = PublishPayload(b'0123456789') publish = PublishPacket(variable_header=variable_header, payload=payload) out = publish.to_bytes() self.assertEqual(out, b'\x30\x13\x00\x05topic\00\x0a0123456789') def test_build(self): packet = PublishPacket.build('/topic', b'data', 1, False, QOS_0, False) self.assertEqual(packet.packet_id, 1) self.assertFalse(packet.dup_flag) self.assertEqual(packet.qos, QOS_0) self.assertFalse(packet.retain_flag) packet = PublishPacket.build('/topic', b'data', 1, False, QOS_1, False) self.assertEqual(packet.packet_id, 1) self.assertFalse(packet.dup_flag) self.assertEqual(packet.qos, QOS_1) self.assertFalse(packet.retain_flag) packet = PublishPacket.build('/topic', b'data', 1, False, QOS_2, False) self.assertEqual(packet.packet_id, 1) self.assertFalse(packet.dup_flag) self.assertEqual(packet.qos, QOS_2) self.assertFalse(packet.retain_flag) packet = PublishPacket.build('/topic', b'data', 1, True, QOS_0, False) self.assertEqual(packet.packet_id, 1) self.assertTrue(packet.dup_flag) self.assertEqual(packet.qos, QOS_0) self.assertFalse(packet.retain_flag) packet = PublishPacket.build('/topic', b'data', 1, True, QOS_1, False) self.assertEqual(packet.packet_id, 1) self.assertTrue(packet.dup_flag) self.assertEqual(packet.qos, QOS_1) self.assertFalse(packet.retain_flag) packet = PublishPacket.build('/topic', b'data', 1, True, QOS_2, False) self.assertEqual(packet.packet_id, 1) self.assertTrue(packet.dup_flag) self.assertEqual(packet.qos, QOS_2) self.assertFalse(packet.retain_flag) packet = PublishPacket.build('/topic', b'data', 1, False, QOS_0, True) self.assertEqual(packet.packet_id, 1) self.assertFalse(packet.dup_flag) self.assertEqual(packet.qos, QOS_0) self.assertTrue(packet.retain_flag) packet = PublishPacket.build('/topic', b'data', 1, False, QOS_1, True) self.assertEqual(packet.packet_id, 1) self.assertFalse(packet.dup_flag) self.assertEqual(packet.qos, QOS_1) self.assertTrue(packet.retain_flag) packet = PublishPacket.build('/topic', b'data', 1, False, QOS_2, True) self.assertEqual(packet.packet_id, 1) self.assertFalse(packet.dup_flag) self.assertEqual(packet.qos, QOS_2) self.assertTrue(packet.retain_flag) packet = PublishPacket.build('/topic', b'data', 1, True, QOS_0, True) self.assertEqual(packet.packet_id, 1) self.assertTrue(packet.dup_flag) self.assertEqual(packet.qos, QOS_0) self.assertTrue(packet.retain_flag) packet = PublishPacket.build('/topic', b'data', 1, True, QOS_1, True) self.assertEqual(packet.packet_id, 1) self.assertTrue(packet.dup_flag) self.assertEqual(packet.qos, QOS_1) self.assertTrue(packet.retain_flag) packet = PublishPacket.build('/topic', b'data', 1, True, QOS_2, True) self.assertEqual(packet.packet_id, 1) self.assertTrue(packet.dup_flag) self.assertEqual(packet.qos, QOS_2) self.assertTrue(packet.retain_flag) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1579961068.0 hbmqtt-0.9.6/tests/mqtt/test_pubrec.py0000644000175000017510000000146600000000000017760 0ustar00niconico00000000000000# Copyright (c) 2015 Nicolas JOUANIN # # See the file license.txt for copying permission. import asyncio import unittest from hbmqtt.mqtt.pubrec import PubrecPacket, PacketIdVariableHeader from hbmqtt.adapters import BufferReader class PubrecPacketTest(unittest.TestCase): def setUp(self): self.loop = asyncio.new_event_loop() def test_from_stream(self): data = b'\x50\x02\x00\x0a' stream = BufferReader(data) message = self.loop.run_until_complete(PubrecPacket.from_stream(stream)) self.assertEqual(message.variable_header.packet_id, 10) def test_to_bytes(self): variable_header = PacketIdVariableHeader(10) publish = PubrecPacket(variable_header=variable_header) out = publish.to_bytes() self.assertEqual(out, b'\x50\x02\x00\x0a') ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1579961068.0 hbmqtt-0.9.6/tests/mqtt/test_pubrel.py0000644000175000017510000000146600000000000017771 0ustar00niconico00000000000000# Copyright (c) 2015 Nicolas JOUANIN # # See the file license.txt for copying permission. import asyncio import unittest from hbmqtt.mqtt.pubrel import PubrelPacket, PacketIdVariableHeader from hbmqtt.adapters import BufferReader class PubrelPacketTest(unittest.TestCase): def setUp(self): self.loop = asyncio.new_event_loop() def test_from_stream(self): data = b'\x60\x02\x00\x0a' stream = BufferReader(data) message = self.loop.run_until_complete(PubrelPacket.from_stream(stream)) self.assertEqual(message.variable_header.packet_id, 10) def test_to_bytes(self): variable_header = PacketIdVariableHeader(10) publish = PubrelPacket(variable_header=variable_header) out = publish.to_bytes() self.assertEqual(out, b'\x62\x02\x00\x0a') ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1579961068.0 hbmqtt-0.9.6/tests/mqtt/test_suback.py0000644000175000017510000000261700000000000017747 0ustar00niconico00000000000000# Copyright (c) 2015 Nicolas JOUANIN # # See the file license.txt for copying permission. import asyncio import unittest from hbmqtt.mqtt.suback import SubackPacket, SubackPayload from hbmqtt.mqtt.packet import PacketIdVariableHeader from hbmqtt.adapters import BufferReader class SubackPacketTest(unittest.TestCase): def setUp(self): self.loop = asyncio.new_event_loop() def test_from_stream(self): data = b'\x90\x06\x00\x0a\x00\x01\x02\x80' stream = BufferReader(data) message = self.loop.run_until_complete(SubackPacket.from_stream(stream)) self.assertEqual(message.payload.return_codes[0], SubackPayload.RETURN_CODE_00) self.assertEqual(message.payload.return_codes[1], SubackPayload.RETURN_CODE_01) self.assertEqual(message.payload.return_codes[2], SubackPayload.RETURN_CODE_02) self.assertEqual(message.payload.return_codes[3], SubackPayload.RETURN_CODE_80) def test_to_stream(self): variable_header = PacketIdVariableHeader(10) payload = SubackPayload( [SubackPayload.RETURN_CODE_00, SubackPayload.RETURN_CODE_01, SubackPayload.RETURN_CODE_02, SubackPayload.RETURN_CODE_80 ]) suback = SubackPacket(variable_header=variable_header, payload=payload) out = suback.to_bytes() self.assertEqual(out, b'\x90\x06\x00\x0a\x00\x01\x02\x80') ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1579961068.0 hbmqtt-0.9.6/tests/mqtt/test_subscribe.py0000644000175000017510000000245400000000000020457 0ustar00niconico00000000000000# Copyright (c) 2015 Nicolas JOUANIN # # See the file license.txt for copying permission. import asyncio import unittest from hbmqtt.mqtt.subscribe import SubscribePacket, SubscribePayload from hbmqtt.mqtt.packet import PacketIdVariableHeader from hbmqtt.mqtt.constants import QOS_1, QOS_2 from hbmqtt.adapters import BufferReader class SubscribePacketTest(unittest.TestCase): def setUp(self): self.loop = asyncio.new_event_loop() def test_from_stream(self): data = b'\x80\x0e\x00\x0a\x00\x03a/b\x01\x00\x03c/d\x02' stream = BufferReader(data) message = self.loop.run_until_complete(SubscribePacket.from_stream(stream)) (topic, qos) = message.payload.topics[0] self.assertEqual(topic, 'a/b') self.assertEqual(qos, QOS_1) (topic, qos) = message.payload.topics[1] self.assertEqual(topic, 'c/d') self.assertEqual(qos, QOS_2) def test_to_stream(self): variable_header = PacketIdVariableHeader(10) payload = SubscribePayload( [ ('a/b', QOS_1), ('c/d', QOS_2) ]) publish = SubscribePacket(variable_header=variable_header, payload=payload) out = publish.to_bytes() self.assertEqual(out, b'\x82\x0e\x00\x0a\x00\x03a/b\x01\x00\x03c/d\x02') ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1579961068.0 hbmqtt-0.9.6/tests/mqtt/test_unsuback.py0000644000175000017510000000153700000000000020312 0ustar00niconico00000000000000# Copyright (c) 2015 Nicolas JOUANIN # # See the file license.txt for copying permission. import asyncio import unittest from hbmqtt.mqtt.unsuback import UnsubackPacket from hbmqtt.mqtt.packet import PacketIdVariableHeader from hbmqtt.adapters import BufferReader class UnsubackPacketTest(unittest.TestCase): def setUp(self): self.loop = asyncio.new_event_loop() def test_from_stream(self): data = b'\xb0\x02\x00\x0a' stream = BufferReader(data) message = self.loop.run_until_complete(UnsubackPacket.from_stream(stream)) self.assertEqual(message.variable_header.packet_id, 10) def test_to_stream(self): variable_header = PacketIdVariableHeader(10) publish = UnsubackPacket(variable_header=variable_header) out = publish.to_bytes() self.assertEqual(out, b'\xb0\x02\x00\x0a') ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1579961068.0 hbmqtt-0.9.6/tests/mqtt/test_unsubscribe.py0000644000175000017510000000204400000000000021015 0ustar00niconico00000000000000# Copyright (c) 2015 Nicolas JOUANIN # # See the file license.txt for copying permission. import asyncio import unittest from hbmqtt.mqtt.unsubscribe import UnsubscribePacket, UnubscribePayload from hbmqtt.mqtt.packet import PacketIdVariableHeader from hbmqtt.adapters import BufferReader class UnsubscribePacketTest(unittest.TestCase): def setUp(self): self.loop = asyncio.new_event_loop() def test_from_stream(self): data = b'\xa2\x0c\x00\n\x00\x03a/b\x00\x03c/d' stream = BufferReader(data) message = self.loop.run_until_complete(UnsubscribePacket.from_stream(stream)) self.assertEqual(message.payload.topics[0], 'a/b') self.assertEqual(message.payload.topics[1], 'c/d') def test_to_stream(self): variable_header = PacketIdVariableHeader(10) payload = UnubscribePayload(['a/b', 'c/d']) publish = UnsubscribePacket(variable_header=variable_header, payload=payload) out = publish.to_bytes() self.assertEqual(out, b'\xa2\x0c\x00\n\x00\x03a/b\x00\x03c/d') ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1579961563.696805 hbmqtt-0.9.6/tests/plugins/0000755000175000017510000000000000000000000015554 5ustar00niconico00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1579961068.0 hbmqtt-0.9.6/tests/plugins/__init__.py0000644000175000017510000000002400000000000017661 0ustar00niconico00000000000000__author__ = 'nico' ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1579961068.0 hbmqtt-0.9.6/tests/plugins/passwd0000644000175000017510000000036400000000000017003 0ustar00niconico00000000000000# Test password file user:$6$1sSXVMBVKMF$uDStq59YfiuFxVF1Gi/.i/Li7Vwf5iTwg8LovLKmCvM5FsRNJM.OPWHhXwI2.4AscLZXSPQVxpIlX6laUl9570 test_user:$6$.c9f9sAzs5YXX2de$GSdOi3iFwHJRCIJn1W63muDFQAL29yoFmU/TXcwDB42F2BZg3zcN5uKBM0.1PjwdMpWHRydbhXWSc3uWKSmKr.././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1579961068.0 hbmqtt-0.9.6/tests/plugins/test_authentication.py0000644000175000017510000000705400000000000022212 0ustar00niconico00000000000000# Copyright (c) 2015 Nicolas JOUANIN # # See the file license.txt for copying permission. import unittest import logging import os import asyncio from hbmqtt.plugins.manager import BaseContext from hbmqtt.plugins.authentication import AnonymousAuthPlugin, FileAuthPlugin from hbmqtt.session import Session formatter = "[%(asctime)s] %(name)s {%(filename)s:%(lineno)d} %(levelname)s - %(message)s" logging.basicConfig(level=logging.DEBUG, format=formatter) class TestAnonymousAuthPlugin(unittest.TestCase): def setUp(self): self.loop = asyncio.new_event_loop() def test_allow_anonymous(self): context = BaseContext() context.logger = logging.getLogger(__name__) context.config = { 'auth': { 'allow-anonymous': True } } s = Session() s.username = "" auth_plugin = AnonymousAuthPlugin(context) ret = self.loop.run_until_complete(auth_plugin.authenticate(session=s)) self.assertTrue(ret) def test_disallow_anonymous(self): context = BaseContext() context.logger = logging.getLogger(__name__) context.config = { 'auth': { 'allow-anonymous': False } } s = Session() s.username = "" auth_plugin = AnonymousAuthPlugin(context) ret = self.loop.run_until_complete(auth_plugin.authenticate(session=s)) self.assertFalse(ret) def test_allow_nonanonymous(self): context = BaseContext() context.logger = logging.getLogger(__name__) context.config = { 'auth': { 'allow-anonymous': False } } s = Session() s.username = "test" auth_plugin = AnonymousAuthPlugin(context) ret = self.loop.run_until_complete(auth_plugin.authenticate(session=s)) self.assertTrue(ret) class TestFileAuthPlugin(unittest.TestCase): def setUp(self): self.loop = asyncio.new_event_loop() def test_allow(self): context = BaseContext() context.logger = logging.getLogger(__name__) context.config = { 'auth': { 'password-file': os.path.join(os.path.dirname(os.path.realpath(__file__)), "passwd") } } s = Session() s.username = "user" s.password = "test" auth_plugin = FileAuthPlugin(context) ret = self.loop.run_until_complete(auth_plugin.authenticate(session=s)) self.assertTrue(ret) def test_wrong_password(self): context = BaseContext() context.logger = logging.getLogger(__name__) context.config = { 'auth': { 'password-file': os.path.join(os.path.dirname(os.path.realpath(__file__)), "passwd") } } s = Session() s.username = "user" s.password = "wrong password" auth_plugin = FileAuthPlugin(context) ret = self.loop.run_until_complete(auth_plugin.authenticate(session=s)) self.assertFalse(ret) def test_unknown_password(self): context = BaseContext() context.logger = logging.getLogger(__name__) context.config = { 'auth': { 'password-file': os.path.join(os.path.dirname(os.path.realpath(__file__)), "passwd") } } s = Session() s.username = "some user" s.password = "some password" auth_plugin = FileAuthPlugin(context) ret = self.loop.run_until_complete(auth_plugin.authenticate(session=s)) self.assertFalse(ret) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1579961068.0 hbmqtt-0.9.6/tests/plugins/test_manager.py0000644000175000017510000000634300000000000020605 0ustar00niconico00000000000000# Copyright (c) 2015 Nicolas JOUANIN # # See the file license.txt for copying permission. import unittest import logging import asyncio from hbmqtt.plugins.manager import PluginManager formatter = "[%(asctime)s] %(name)s {%(filename)s:%(lineno)d} %(levelname)s - %(message)s" logging.basicConfig(level=logging.INFO, format=formatter) class TestPlugin: def __init__(self, context): self.context = context class EventTestPlugin: def __init__(self, context): self.context = context self.test_flag = False self.coro_flag = False @asyncio.coroutine def on_test(self, *args, **kwargs): self.test_flag = True self.context.logger.info("on_test") @asyncio.coroutine def test_coro(self, *args, **kwargs): self.coro_flag = True @asyncio.coroutine def ret_coro(self, *args, **kwargs): return "TEST" class TestPluginManager(unittest.TestCase): def setUp(self): self.loop = asyncio.new_event_loop() def test_load_plugin(self): manager = PluginManager("hbmqtt.test.plugins", context=None) self.assertTrue(len(manager._plugins) > 0) def test_fire_event(self): @asyncio.coroutine def fire_event(): yield from manager.fire_event("test") yield from asyncio.sleep(1, loop=self.loop) yield from manager.close() manager = PluginManager("hbmqtt.test.plugins", context=None, loop=self.loop) self.loop.run_until_complete(fire_event()) plugin = manager.get_plugin("event_plugin") self.assertTrue(plugin.object.test_flag) def test_fire_event_wait(self): @asyncio.coroutine def fire_event(): yield from manager.fire_event("test", wait=True) yield from manager.close() manager = PluginManager("hbmqtt.test.plugins", context=None, loop=self.loop) self.loop.run_until_complete(fire_event()) plugin = manager.get_plugin("event_plugin") self.assertTrue(plugin.object.test_flag) def test_map_coro(self): @asyncio.coroutine def call_coro(): yield from manager.map_plugin_coro('test_coro') manager = PluginManager("hbmqtt.test.plugins", context=None, loop=self.loop) self.loop.run_until_complete(call_coro()) plugin = manager.get_plugin("event_plugin") self.assertTrue(plugin.object.test_coro) def test_map_coro_return(self): @asyncio.coroutine def call_coro(): return (yield from manager.map_plugin_coro('ret_coro')) manager = PluginManager("hbmqtt.test.plugins", context=None, loop=self.loop) ret = self.loop.run_until_complete(call_coro()) plugin = manager.get_plugin("event_plugin") self.assertEqual(ret[plugin], "TEST") def test_map_coro_filter(self): """ Run plugin coro but expect no return as an empty filter is given :return: """ @asyncio.coroutine def call_coro(): return (yield from manager.map_plugin_coro('ret_coro', filter_plugins=[])) manager = PluginManager("hbmqtt.test.plugins", context=None, loop=self.loop) ret = self.loop.run_until_complete(call_coro()) self.assertTrue(len(ret) == 0) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1579961068.0 hbmqtt-0.9.6/tests/plugins/test_persistence.py0000644000175000017510000000364400000000000021520 0ustar00niconico00000000000000# Copyright (c) 2015 Nicolas JOUANIN # # See the file license.txt for copying permission. import unittest import logging import os import asyncio import sqlite3 from hbmqtt.plugins.manager import BaseContext from hbmqtt.plugins.persistence import SQLitePlugin formatter = "[%(asctime)s] %(name)s {%(filename)s:%(lineno)d} %(levelname)s - %(message)s" logging.basicConfig(level=logging.DEBUG, format=formatter) class TestSQLitePlugin(unittest.TestCase): def setUp(self): self.loop = asyncio.new_event_loop() def test_create_tables(self): dbfile = os.path.join(os.path.dirname(os.path.realpath(__file__)), "test.db") context = BaseContext() context.logger = logging.getLogger(__name__) context.config = { 'persistence': { 'file': dbfile } } SQLitePlugin(context) conn = sqlite3.connect(dbfile) cursor = conn.cursor() rows = cursor.execute("SELECT name FROM sqlite_master where type = 'table'") tables = [] for row in rows: tables.append(row[0]) self.assertIn("session", tables) # def test_save_session(self): # dbfile = os.path.join(os.path.dirname(os.path.realpath(__file__)), "test.db") # context = BaseContext() # context.logger = logging.getLogger(__name__) # context.config = { # 'persistence': { # 'file': dbfile # } # } # sql_plugin = SQLitePlugin(context) # s = Session() # s.client_id = 'test_save_session' # ret = self.loop.run_until_complete(sql_plugin.save_session(session=s)) # # conn = sqlite3.connect(dbfile) # cursor = conn.cursor() # row = cursor.execute("SELECT client_id FROM session where client_id = 'test_save_session'").fetchone() # self.assertTrue(len(row) == 1) # self.assertEqual(row[0], s.client_id) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1579961068.0 hbmqtt-0.9.6/tests/test_broker.py0000644000175000017510000007474600000000000017012 0ustar00niconico00000000000000# Copyright (c) 2015 Nicolas JOUANIN # # See the file license.txt for copying permission. import asyncio import logging import unittest from unittest.mock import patch, call from hbmqtt.adapters import StreamReaderAdapter, StreamWriterAdapter from hbmqtt.broker import ( EVENT_BROKER_PRE_START, EVENT_BROKER_POST_START, EVENT_BROKER_PRE_SHUTDOWN, EVENT_BROKER_POST_SHUTDOWN, EVENT_BROKER_CLIENT_CONNECTED, EVENT_BROKER_CLIENT_DISCONNECTED, EVENT_BROKER_CLIENT_SUBSCRIBED, EVENT_BROKER_CLIENT_UNSUBSCRIBED, EVENT_BROKER_MESSAGE_RECEIVED, Broker) from hbmqtt.client import MQTTClient, ConnectException from hbmqtt.mqtt import ( ConnectPacket, ConnackPacket, PublishPacket, PubrecPacket, PubrelPacket, PubcompPacket, DisconnectPacket) from hbmqtt.mqtt.connect import ConnectVariableHeader, ConnectPayload from hbmqtt.mqtt.constants import QOS_0, QOS_1, QOS_2 formatter = "[%(asctime)s] %(name)s {%(filename)s:%(lineno)d} %(levelname)s - %(message)s" logging.basicConfig(level=logging.DEBUG, format=formatter) log = logging.getLogger(__name__) test_config = { 'listeners': { 'default': { 'type': 'tcp', 'bind': '127.0.0.1:1883', 'max_connections': 10 }, }, 'sys_interval': 0, 'auth': { 'allow-anonymous': True, } } #class AsyncMock(MagicMock): # def __yield from__(self, *args, **kwargs): # future = asyncio.Future() # future.set_result(self) # result = yield from future # return result class BrokerTest(unittest.TestCase): def setUp(self): self.loop = asyncio.new_event_loop() asyncio.set_event_loop(self.loop) def tearDown(self): self.loop.close() @patch('hbmqtt.broker.PluginManager') def test_start_stop(self, MockPluginManager): @asyncio.coroutine def test_coro(): try: broker = Broker(test_config, plugin_namespace="hbmqtt.test.plugins") yield from broker.start() self.assertTrue(broker.transitions.is_started()) self.assertDictEqual(broker._sessions, {}) self.assertIn('default', broker._servers) MockPluginManager.assert_has_calls( [call().fire_event(EVENT_BROKER_PRE_START), call().fire_event(EVENT_BROKER_POST_START)], any_order=True) MockPluginManager.reset_mock() yield from broker.shutdown() MockPluginManager.assert_has_calls( [call().fire_event(EVENT_BROKER_PRE_SHUTDOWN), call().fire_event(EVENT_BROKER_POST_SHUTDOWN)], any_order=True) self.assertTrue(broker.transitions.is_stopped()) future.set_result(True) except Exception as ae: future.set_exception(ae) future = asyncio.Future(loop=self.loop) self.loop.run_until_complete(test_coro()) if future.exception(): raise future.exception() @patch('hbmqtt.broker.PluginManager') def test_client_connect(self, MockPluginManager): @asyncio.coroutine def test_coro(): try: broker = Broker(test_config, plugin_namespace="hbmqtt.test.plugins") yield from broker.start() self.assertTrue(broker.transitions.is_started()) client = MQTTClient() ret = yield from client.connect('mqtt://127.0.0.1/') self.assertEqual(ret, 0) self.assertIn(client.session.client_id, broker._sessions) yield from client.disconnect() yield from asyncio.sleep(0.1) yield from broker.shutdown() self.assertTrue(broker.transitions.is_stopped()) self.assertDictEqual(broker._sessions, {}) MockPluginManager.assert_has_calls( [call().fire_event(EVENT_BROKER_CLIENT_CONNECTED, client_id=client.session.client_id), call().fire_event(EVENT_BROKER_CLIENT_DISCONNECTED, client_id=client.session.client_id)], any_order=True) future.set_result(True) except Exception as ae: future.set_exception(ae) future = asyncio.Future(loop=self.loop) self.loop.run_until_complete(test_coro()) if future.exception(): raise future.exception() @patch('hbmqtt.broker.PluginManager') def test_client_connect_will_flag(self, MockPluginManager): @asyncio.coroutine def test_coro(): try: broker = Broker(test_config, plugin_namespace="hbmqtt.test.plugins") yield from broker.start() self.assertTrue(broker.transitions.is_started()) conn_reader, conn_writer = \ yield from asyncio.open_connection('127.0.0.1', 1883, loop=self.loop) reader = StreamReaderAdapter(conn_reader) writer = StreamWriterAdapter(conn_writer) vh = ConnectVariableHeader() payload = ConnectPayload() vh.keep_alive = 10 vh.clean_session_flag = False vh.will_retain_flag = False vh.will_flag = True vh.will_qos = QOS_0 payload.client_id = 'test_id' payload.will_message = b'test' payload.will_topic = '/topic' connect = ConnectPacket(vh=vh, payload=payload) yield from connect.to_stream(writer) yield from ConnackPacket.from_stream(reader) yield from asyncio.sleep(0.1) disconnect = DisconnectPacket() yield from disconnect.to_stream(writer) yield from asyncio.sleep(0.1) yield from broker.shutdown() self.assertTrue(broker.transitions.is_stopped()) self.assertDictEqual(broker._sessions, {}) future.set_result(True) except Exception as ae: future.set_exception(ae) future = asyncio.Future(loop=self.loop) self.loop.run_until_complete(test_coro()) if future.exception(): raise future.exception() @patch('hbmqtt.broker.PluginManager') def test_client_connect_clean_session_false(self, MockPluginManager): @asyncio.coroutine def test_coro(): try: broker = Broker(test_config, plugin_namespace="hbmqtt.test.plugins") yield from broker.start() self.assertTrue(broker.transitions.is_started()) client = MQTTClient(client_id="", config={'auto_reconnect': False}) return_code = None try: yield from client.connect('mqtt://127.0.0.1/', cleansession=False) except ConnectException as ce: return_code = ce.return_code self.assertEqual(return_code, 0x02) self.assertNotIn(client.session.client_id, broker._sessions) yield from client.disconnect() yield from asyncio.sleep(0.1) yield from broker.shutdown() future.set_result(True) except Exception as ae: future.set_exception(ae) future = asyncio.Future(loop=self.loop) self.loop.run_until_complete(test_coro()) if future.exception(): raise future.exception() @patch('hbmqtt.broker.PluginManager') def test_client_subscribe(self, MockPluginManager): @asyncio.coroutine def test_coro(): try: broker = Broker(test_config, plugin_namespace="hbmqtt.test.plugins") yield from broker.start() self.assertTrue(broker.transitions.is_started()) client = MQTTClient() ret = yield from client.connect('mqtt://127.0.0.1/') self.assertEqual(ret, 0) yield from client.subscribe([('/topic', QOS_0)]) # Test if the client test client subscription is registered self.assertIn('/topic', broker._subscriptions) subs = broker._subscriptions['/topic'] self.assertEqual(len(subs), 1) (s, qos) = subs[0] self.assertEqual(s, client.session) self.assertEqual(qos, QOS_0) yield from client.disconnect() yield from asyncio.sleep(0.1) yield from broker.shutdown() self.assertTrue(broker.transitions.is_stopped()) MockPluginManager.assert_has_calls( [call().fire_event(EVENT_BROKER_CLIENT_SUBSCRIBED, client_id=client.session.client_id, topic='/topic', qos=QOS_0)], any_order=True) future.set_result(True) except Exception as ae: future.set_exception(ae) future = asyncio.Future(loop=self.loop) self.loop.run_until_complete(test_coro()) if future.exception(): raise future.exception() @patch('hbmqtt.broker.PluginManager') def test_client_subscribe_twice(self, MockPluginManager): @asyncio.coroutine def test_coro(): try: broker = Broker(test_config, plugin_namespace="hbmqtt.test.plugins") yield from broker.start() self.assertTrue(broker.transitions.is_started()) client = MQTTClient() ret = yield from client.connect('mqtt://127.0.0.1/') self.assertEqual(ret, 0) yield from client.subscribe([('/topic', QOS_0)]) # Test if the client test client subscription is registered self.assertIn('/topic', broker._subscriptions) subs = broker._subscriptions['/topic'] self.assertEqual(len(subs), 1) (s, qos) = subs[0] self.assertEqual(s, client.session) self.assertEqual(qos, QOS_0) yield from client.subscribe([('/topic', QOS_0)]) self.assertEqual(len(subs), 1) (s, qos) = subs[0] self.assertEqual(s, client.session) self.assertEqual(qos, QOS_0) yield from client.disconnect() yield from asyncio.sleep(0.1) yield from broker.shutdown() self.assertTrue(broker.transitions.is_stopped()) MockPluginManager.assert_has_calls( [call().fire_event(EVENT_BROKER_CLIENT_SUBSCRIBED, client_id=client.session.client_id, topic='/topic', qos=QOS_0)], any_order=True) future.set_result(True) except Exception as ae: future.set_exception(ae) future = asyncio.Future(loop=self.loop) self.loop.run_until_complete(test_coro()) if future.exception(): raise future.exception() @patch('hbmqtt.broker.PluginManager') def test_client_unsubscribe(self, MockPluginManager): @asyncio.coroutine def test_coro(): try: broker = Broker(test_config, plugin_namespace="hbmqtt.test.plugins") yield from broker.start() self.assertTrue(broker.transitions.is_started()) client = MQTTClient() ret = yield from client.connect('mqtt://127.0.0.1/') self.assertEqual(ret, 0) yield from client.subscribe([('/topic', QOS_0)]) # Test if the client test client subscription is registered self.assertIn('/topic', broker._subscriptions) subs = broker._subscriptions['/topic'] self.assertEqual(len(subs), 1) (s, qos) = subs[0] self.assertEqual(s, client.session) self.assertEqual(qos, QOS_0) yield from client.unsubscribe(['/topic']) yield from asyncio.sleep(0.1) self.assertEqual(broker._subscriptions['/topic'], []) yield from client.disconnect() yield from asyncio.sleep(0.1) yield from broker.shutdown() self.assertTrue(broker.transitions.is_stopped()) MockPluginManager.assert_has_calls( [ call().fire_event(EVENT_BROKER_CLIENT_SUBSCRIBED, client_id=client.session.client_id, topic='/topic', qos=QOS_0), call().fire_event(EVENT_BROKER_CLIENT_UNSUBSCRIBED, client_id=client.session.client_id, topic='/topic') ], any_order=True) future.set_result(True) except Exception as ae: future.set_exception(ae) future = asyncio.Future(loop=self.loop) self.loop.run_until_complete(test_coro()) if future.exception(): raise future.exception() @patch('hbmqtt.broker.PluginManager') def test_client_publish(self, MockPluginManager): @asyncio.coroutine def test_coro(): try: broker = Broker(test_config, plugin_namespace="hbmqtt.test.plugins") yield from broker.start() self.assertTrue(broker.transitions.is_started()) pub_client = MQTTClient() ret = yield from pub_client.connect('mqtt://127.0.0.1/') self.assertEqual(ret, 0) ret_message = yield from pub_client.publish('/topic', b'data', QOS_0) yield from pub_client.disconnect() self.assertEqual(broker._retained_messages, {}) yield from asyncio.sleep(0.1) yield from broker.shutdown() self.assertTrue(broker.transitions.is_stopped()) MockPluginManager.assert_has_calls( [ call().fire_event(EVENT_BROKER_MESSAGE_RECEIVED, client_id=pub_client.session.client_id, message=ret_message), ], any_order=True) future.set_result(True) except Exception as ae: future.set_exception(ae) future = asyncio.Future(loop=self.loop) self.loop.run_until_complete(test_coro()) if future.exception(): raise future.exception() #@patch('hbmqtt.broker.PluginManager') def test_client_publish_dup(self): @asyncio.coroutine def test_coro(): try: broker = Broker(test_config, plugin_namespace="hbmqtt.test.plugins") yield from broker.start() self.assertTrue(broker.transitions.is_started()) conn_reader, conn_writer = \ yield from asyncio.open_connection('127.0.0.1', 1883, loop=self.loop) reader = StreamReaderAdapter(conn_reader) writer = StreamWriterAdapter(conn_writer) vh = ConnectVariableHeader() payload = ConnectPayload() vh.keep_alive = 10 vh.clean_session_flag = False vh.will_retain_flag = False payload.client_id = 'test_id' connect = ConnectPacket(vh=vh, payload=payload) yield from connect.to_stream(writer) yield from ConnackPacket.from_stream(reader) publish_1 = PublishPacket.build('/test', b'data', 1, False, QOS_2, False) yield from publish_1.to_stream(writer) asyncio.ensure_future(PubrecPacket.from_stream(reader), loop=self.loop) yield from asyncio.sleep(2) publish_dup = PublishPacket.build('/test', b'data', 1, True, QOS_2, False) yield from publish_dup.to_stream(writer) yield from PubrecPacket.from_stream(reader) pubrel = PubrelPacket.build(1) yield from pubrel.to_stream(writer) yield from PubcompPacket.from_stream(reader) disconnect = DisconnectPacket() yield from disconnect.to_stream(writer) yield from asyncio.sleep(0.1) yield from broker.shutdown() future.set_result(True) except Exception as ae: future.set_exception(ae) future = asyncio.Future(loop=self.loop) self.loop.run_until_complete(test_coro()) if future.exception(): raise future.exception() @patch('hbmqtt.broker.PluginManager') def test_client_publish_invalid_topic(self, MockPluginManager): @asyncio.coroutine def test_coro(): try: broker = Broker(test_config, plugin_namespace="hbmqtt.test.plugins") yield from broker.start() self.assertTrue(broker.transitions.is_started()) pub_client = MQTTClient() ret = yield from pub_client.connect('mqtt://127.0.0.1/') self.assertEqual(ret, 0) yield from pub_client.publish('/+', b'data', QOS_0) yield from asyncio.sleep(0.1) yield from pub_client.disconnect() yield from asyncio.sleep(0.1) yield from broker.shutdown() self.assertTrue(broker.transitions.is_stopped()) future.set_result(True) except Exception as ae: future.set_exception(ae) future = asyncio.Future(loop=self.loop) self.loop.run_until_complete(test_coro()) if future.exception(): raise future.exception() @patch('hbmqtt.broker.PluginManager') def test_client_publish_big(self, MockPluginManager): @asyncio.coroutine def test_coro(): try: broker = Broker(test_config, plugin_namespace="hbmqtt.test.plugins") yield from broker.start() self.assertTrue(broker.transitions.is_started()) pub_client = MQTTClient() ret = yield from pub_client.connect('mqtt://127.0.0.1/') self.assertEqual(ret, 0) ret_message = yield from pub_client.publish('/topic', bytearray(b'\x99' * 256 * 1024), QOS_2) yield from pub_client.disconnect() self.assertEqual(broker._retained_messages, {}) yield from asyncio.sleep(0.1) yield from broker.shutdown() self.assertTrue(broker.transitions.is_stopped()) MockPluginManager.assert_has_calls( [ call().fire_event(EVENT_BROKER_MESSAGE_RECEIVED, client_id=pub_client.session.client_id, message=ret_message), ], any_order=True) future.set_result(True) except Exception as ae: future.set_exception(ae) future = asyncio.Future(loop=self.loop) self.loop.run_until_complete(test_coro()) if future.exception(): raise future.exception() @patch('hbmqtt.broker.PluginManager') def test_client_publish_retain(self, MockPluginManager): @asyncio.coroutine def test_coro(): try: broker = Broker(test_config, plugin_namespace="hbmqtt.test.plugins") yield from broker.start() self.assertTrue(broker.transitions.is_started()) pub_client = MQTTClient() ret = yield from pub_client.connect('mqtt://127.0.0.1/') self.assertEqual(ret, 0) yield from pub_client.publish('/topic', b'data', QOS_0, retain=True) yield from pub_client.disconnect() yield from asyncio.sleep(0.1) self.assertIn('/topic', broker._retained_messages) retained_message = broker._retained_messages['/topic'] self.assertEqual(retained_message.source_session, pub_client.session) self.assertEqual(retained_message.topic, '/topic') self.assertEqual(retained_message.data, b'data') self.assertEqual(retained_message.qos, QOS_0) yield from broker.shutdown() self.assertTrue(broker.transitions.is_stopped()) future.set_result(True) except Exception as ae: future.set_exception(ae) future = asyncio.Future(loop=self.loop) self.loop.run_until_complete(test_coro()) if future.exception(): raise future.exception() @patch('hbmqtt.broker.PluginManager') def test_client_publish_retain_delete(self, MockPluginManager): @asyncio.coroutine def test_coro(): try: broker = Broker(test_config, plugin_namespace="hbmqtt.test.plugins") yield from broker.start() self.assertTrue(broker.transitions.is_started()) pub_client = MQTTClient() ret = yield from pub_client.connect('mqtt://127.0.0.1/') self.assertEqual(ret, 0) yield from pub_client.publish('/topic', b'', QOS_0, retain=True) yield from pub_client.disconnect() yield from asyncio.sleep(0.1) self.assertNotIn('/topic', broker._retained_messages) yield from broker.shutdown() self.assertTrue(broker.transitions.is_stopped()) future.set_result(True) except Exception as ae: future.set_exception(ae) future = asyncio.Future(loop=self.loop) self.loop.run_until_complete(test_coro()) if future.exception(): raise future.exception() @patch('hbmqtt.broker.PluginManager') def test_client_subscribe_publish(self, MockPluginManager): @asyncio.coroutine def test_coro(): try: broker = Broker(test_config, plugin_namespace="hbmqtt.test.plugins") yield from broker.start() self.assertTrue(broker.transitions.is_started()) sub_client = MQTTClient() yield from sub_client.connect('mqtt://127.0.0.1') ret = yield from sub_client.subscribe([('/qos0', QOS_0), ('/qos1', QOS_1), ('/qos2', QOS_2)]) self.assertEqual(ret, [QOS_0, QOS_1, QOS_2]) yield from self._client_publish('/qos0', b'data', QOS_0) yield from self._client_publish('/qos1', b'data', QOS_1) yield from self._client_publish('/qos2', b'data', QOS_2) yield from asyncio.sleep(0.1) for qos in [QOS_0, QOS_1, QOS_2]: message = yield from sub_client.deliver_message() self.assertIsNotNone(message) self.assertEqual(message.topic, '/qos%s' % qos) self.assertEqual(message.data, b'data') self.assertEqual(message.qos, qos) yield from sub_client.disconnect() yield from asyncio.sleep(0.1) yield from broker.shutdown() self.assertTrue(broker.transitions.is_stopped()) future.set_result(True) except Exception as ae: future.set_exception(ae) future = asyncio.Future(loop=self.loop) self.loop.run_until_complete(test_coro()) if future.exception(): raise future.exception() @patch('hbmqtt.broker.PluginManager') def test_client_subscribe_invalid(self, MockPluginManager): @asyncio.coroutine def test_coro(): try: broker = Broker(test_config, plugin_namespace="hbmqtt.test.plugins") yield from broker.start() self.assertTrue(broker.transitions.is_started()) sub_client = MQTTClient() yield from sub_client.connect('mqtt://127.0.0.1') ret = yield from sub_client.subscribe( [('+', QOS_0), ('+/tennis/#', QOS_0), ('sport+', QOS_0), ('sport/+/player1', QOS_0)]) self.assertEqual(ret, [QOS_0, QOS_0, 0x80, QOS_0]) yield from asyncio.sleep(0.1) yield from sub_client.disconnect() yield from asyncio.sleep(0.1) yield from broker.shutdown() self.assertTrue(broker.transitions.is_stopped()) future.set_result(True) except Exception as ae: future.set_exception(ae) future = asyncio.Future(loop=self.loop) self.loop.run_until_complete(test_coro()) if future.exception(): raise future.exception() @patch('hbmqtt.broker.PluginManager') def test_client_subscribe_publish_dollar_topic_1(self, MockPluginManager): @asyncio.coroutine def test_coro(): try: broker = Broker(test_config, plugin_namespace="hbmqtt.test.plugins") yield from broker.start() self.assertTrue(broker.transitions.is_started()) sub_client = MQTTClient() yield from sub_client.connect('mqtt://127.0.0.1') ret = yield from sub_client.subscribe([('#', QOS_0)]) self.assertEqual(ret, [QOS_0]) yield from self._client_publish('/topic', b'data', QOS_0) message = yield from sub_client.deliver_message() self.assertIsNotNone(message) yield from self._client_publish('$topic', b'data', QOS_0) yield from asyncio.sleep(0.1) message = None try: message = yield from sub_client.deliver_message(timeout=2) except Exception as e: pass self.assertIsNone(message) yield from sub_client.disconnect() yield from asyncio.sleep(0.1) yield from broker.shutdown() self.assertTrue(broker.transitions.is_stopped()) future.set_result(True) except Exception as ae: future.set_exception(ae) future = asyncio.Future(loop=self.loop) self.loop.run_until_complete(test_coro()) if future.exception(): raise future.exception() @patch('hbmqtt.broker.PluginManager') def test_client_subscribe_publish_dollar_topic_2(self, MockPluginManager): @asyncio.coroutine def test_coro(): try: broker = Broker(test_config, plugin_namespace="hbmqtt.test.plugins") yield from broker.start() self.assertTrue(broker.transitions.is_started()) sub_client = MQTTClient() yield from sub_client.connect('mqtt://127.0.0.1') ret = yield from sub_client.subscribe([('+/monitor/Clients', QOS_0)]) self.assertEqual(ret, [QOS_0]) yield from self._client_publish('/test/monitor/Clients', b'data', QOS_0) message = yield from sub_client.deliver_message() self.assertIsNotNone(message) yield from self._client_publish('$SYS/monitor/Clients', b'data', QOS_0) yield from asyncio.sleep(0.1) message = None try: message = yield from sub_client.deliver_message(timeout=2) except Exception as e: pass self.assertIsNone(message) yield from sub_client.disconnect() yield from asyncio.sleep(0.1) yield from broker.shutdown() self.assertTrue(broker.transitions.is_stopped()) future.set_result(True) except Exception as ae: future.set_exception(ae) future = asyncio.Future(loop=self.loop) self.loop.run_until_complete(test_coro()) if future.exception(): raise future.exception() @patch('hbmqtt.broker.PluginManager') def test_client_publish_retain_subscribe(self, MockPluginManager): @asyncio.coroutine def test_coro(): try: broker = Broker(test_config, plugin_namespace="hbmqtt.test.plugins") yield from broker.start() self.assertTrue(broker.transitions.is_started()) sub_client = MQTTClient() yield from sub_client.connect('mqtt://127.0.0.1', cleansession=False) ret = yield from sub_client.subscribe([('/qos0', QOS_0), ('/qos1', QOS_1), ('/qos2', QOS_2)]) self.assertEqual(ret, [QOS_0, QOS_1, QOS_2]) yield from sub_client.disconnect() yield from asyncio.sleep(0.1) yield from self._client_publish('/qos0', b'data', QOS_0, retain=True) yield from self._client_publish('/qos1', b'data', QOS_1, retain=True) yield from self._client_publish('/qos2', b'data', QOS_2, retain=True) yield from sub_client.reconnect() for qos in [QOS_0, QOS_1, QOS_2]: log.debug("TEST QOS: %d" % qos) message = yield from sub_client.deliver_message() log.debug("Message: " + repr(message.publish_packet)) self.assertIsNotNone(message) self.assertEqual(message.topic, '/qos%s' % qos) self.assertEqual(message.data, b'data') self.assertEqual(message.qos, qos) yield from sub_client.disconnect() yield from asyncio.sleep(0.1) yield from broker.shutdown() self.assertTrue(broker.transitions.is_stopped()) future.set_result(True) except Exception as ae: future.set_exception(ae) future = asyncio.Future(loop=self.loop) self.loop.run_until_complete(test_coro()) if future.exception(): raise future.exception() @asyncio.coroutine def _client_publish(self, topic, data, qos, retain=False): pub_client = MQTTClient() ret = yield from pub_client.connect('mqtt://127.0.0.1/') self.assertEqual(ret, 0) ret = yield from pub_client.publish(topic, data, qos, retain) yield from pub_client.disconnect() return ret ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1579961068.0 hbmqtt-0.9.6/tests/test_client.py0000644000175000017510000002572200000000000016772 0ustar00niconico00000000000000# Copyright (c) 2015 Nicolas JOUANIN # # See the file license.txt for copying permission. import unittest import asyncio import os import logging from hbmqtt.client import MQTTClient, ConnectException from hbmqtt.broker import Broker from hbmqtt.mqtt.constants import QOS_0, QOS_1, QOS_2 formatter = "[%(asctime)s] %(name)s {%(filename)s:%(lineno)d} %(levelname)s - %(message)s" logging.basicConfig(level=logging.ERROR, format=formatter) log = logging.getLogger(__name__) broker_config = { 'listeners': { 'default': { 'type': 'tcp', 'bind': '127.0.0.1:1883', 'max_connections': 10 }, 'ws': { 'type': 'ws', 'bind': '127.0.0.1:8080', 'max_connections': 10 }, 'wss': { 'type': 'ws', 'bind': '127.0.0.1:8081', 'max_connections': 10 }, }, 'sys_interval': 0, 'auth': { 'allow-anonymous': True, } } class MQTTClientTest(unittest.TestCase): def setUp(self): self.loop = asyncio.new_event_loop() asyncio.set_event_loop(self.loop) def tearDown(self): self.loop.close() def test_connect_tcp(self): @asyncio.coroutine def test_coro(): try: client = MQTTClient() yield from client.connect('mqtt://test.mosquitto.org/') self.assertIsNotNone(client.session) yield from client.disconnect() future.set_result(True) except Exception as ae: future.set_exception(ae) future = asyncio.Future(loop=self.loop) self.loop.run_until_complete(test_coro()) if future.exception(): raise future.exception() def test_connect_tcp_secure(self): @asyncio.coroutine def test_coro(): try: client = MQTTClient(config={'check_hostname': False}) ca = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'mosquitto.org.crt') yield from client.connect('mqtts://test.mosquitto.org/', cafile=ca) self.assertIsNotNone(client.session) yield from client.disconnect() future.set_result(True) except Exception as ae: future.set_exception(ae) future = asyncio.Future(loop=self.loop) self.loop.run_until_complete(test_coro()) if future.exception(): raise future.exception() def test_connect_tcp_failure(self): @asyncio.coroutine def test_coro(): try: config = {'auto_reconnect': False} client = MQTTClient(config=config) yield from client.connect('mqtt://127.0.0.1/') except ConnectException as e: future.set_result(True) future = asyncio.Future(loop=self.loop) self.loop.run_until_complete(test_coro()) if future.exception(): raise future.exception() def test_connect_ws(self): @asyncio.coroutine def test_coro(): try: broker = Broker(broker_config, plugin_namespace="hbmqtt.test.plugins") yield from broker.start() client = MQTTClient() yield from client.connect('ws://127.0.0.1:8080/') self.assertIsNotNone(client.session) yield from client.disconnect() yield from broker.shutdown() future.set_result(True) except Exception as ae: future.set_exception(ae) future = asyncio.Future(loop=self.loop) self.loop.run_until_complete(test_coro()) if future.exception(): raise future.exception() raise future.exception() def test_reconnect_ws_retain_username_password(self): @asyncio.coroutine def test_coro(): try: broker = Broker(broker_config, plugin_namespace="hbmqtt.test.plugins") yield from broker.start() client = MQTTClient() yield from client.connect('ws://fred:password@127.0.0.1:8080/') self.assertIsNotNone(client.session) yield from client.disconnect() yield from client.reconnect() self.assertIsNotNone(client.session.username) self.assertIsNotNone(client.session.password) yield from broker.shutdown() future.set_result(True) except Exception as ae: future.set_exception(ae) future = asyncio.Future(loop=self.loop) self.loop.run_until_complete(test_coro()) if future.exception(): raise future.exception() def test_connect_ws_secure(self): @asyncio.coroutine def test_coro(): try: broker = Broker(broker_config, plugin_namespace="hbmqtt.test.plugins") yield from broker.start() client = MQTTClient() ca = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'mosquitto.org.crt') yield from client.connect('ws://127.0.0.1:8081/', cafile=ca) self.assertIsNotNone(client.session) yield from client.disconnect() yield from broker.shutdown() future.set_result(True) except Exception as ae: future.set_exception(ae) future = asyncio.Future(loop=self.loop) self.loop.run_until_complete(test_coro()) if future.exception(): raise future.exception() def test_ping(self): @asyncio.coroutine def test_coro(): try: broker = Broker(broker_config, plugin_namespace="hbmqtt.test.plugins") yield from broker.start() client = MQTTClient() yield from client.connect('mqtt://127.0.0.1/') self.assertIsNotNone(client.session) yield from client.ping() yield from client.disconnect() yield from broker.shutdown() future.set_result(True) except Exception as ae: future.set_exception(ae) future = asyncio.Future(loop=self.loop) self.loop.run_until_complete(test_coro()) if future.exception(): raise future.exception() def test_subscribe(self): @asyncio.coroutine def test_coro(): try: broker = Broker(broker_config, plugin_namespace="hbmqtt.test.plugins") yield from broker.start() client = MQTTClient() yield from client.connect('mqtt://127.0.0.1/') self.assertIsNotNone(client.session) ret = yield from client.subscribe([ ('$SYS/broker/uptime', QOS_0), ('$SYS/broker/uptime', QOS_1), ('$SYS/broker/uptime', QOS_2), ]) self.assertEqual(ret[0], QOS_0) self.assertEqual(ret[1], QOS_1) self.assertEqual(ret[2], QOS_2) yield from client.disconnect() yield from broker.shutdown() future.set_result(True) except Exception as ae: future.set_exception(ae) future = asyncio.Future(loop=self.loop) self.loop.run_until_complete(test_coro()) if future.exception(): raise future.exception() def test_unsubscribe(self): @asyncio.coroutine def test_coro(): try: broker = Broker(broker_config, plugin_namespace="hbmqtt.test.plugins") yield from broker.start() client = MQTTClient() yield from client.connect('mqtt://127.0.0.1/') self.assertIsNotNone(client.session) ret = yield from client.subscribe([ ('$SYS/broker/uptime', QOS_0), ]) self.assertEqual(ret[0], QOS_0) yield from client.unsubscribe(['$SYS/broker/uptime']) yield from client.disconnect() yield from broker.shutdown() future.set_result(True) except Exception as ae: future.set_exception(ae) future = asyncio.Future(loop=self.loop) self.loop.run_until_complete(test_coro()) if future.exception(): raise future.exception() def test_deliver(self): data = b'data' @asyncio.coroutine def test_coro(): try: broker = Broker(broker_config, plugin_namespace="hbmqtt.test.plugins") yield from broker.start() client = MQTTClient() yield from client.connect('mqtt://127.0.0.1/') self.assertIsNotNone(client.session) ret = yield from client.subscribe([ ('test_topic', QOS_0), ]) self.assertEqual(ret[0], QOS_0) client_pub = MQTTClient() yield from client_pub.connect('mqtt://127.0.0.1/') yield from client_pub.publish('test_topic', data, QOS_0) yield from client_pub.disconnect() message = yield from client.deliver_message() self.assertIsNotNone(message) self.assertIsNotNone(message.publish_packet) self.assertEqual(message.data, data) yield from client.unsubscribe(['$SYS/broker/uptime']) yield from client.disconnect() yield from broker.shutdown() future.set_result(True) except Exception as ae: future.set_exception(ae) future = asyncio.Future(loop=self.loop) self.loop.run_until_complete(test_coro()) if future.exception(): raise future.exception() def test_deliver_timeout(self): @asyncio.coroutine def test_coro(): try: broker = Broker(broker_config, plugin_namespace="hbmqtt.test.plugins") yield from broker.start() client = MQTTClient() yield from client.connect('mqtt://127.0.0.1/') self.assertIsNotNone(client.session) ret = yield from client.subscribe([ ('test_topic', QOS_0), ]) self.assertEqual(ret[0], QOS_0) with self.assertRaises(asyncio.TimeoutError): yield from client.deliver_message(timeout=2) yield from client.unsubscribe(['$SYS/broker/uptime']) yield from client.disconnect() yield from broker.shutdown() future.set_result(True) except Exception as ae: future.set_exception(ae) future = asyncio.Future(loop=self.loop) self.loop.run_until_complete(test_coro()) if future.exception(): raise future.exception() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1579961068.0 hbmqtt-0.9.6/tests/test_codecs.py0000644000175000017510000000173200000000000016747 0ustar00niconico00000000000000# Copyright (c) 2015 Nicolas JOUANIN # # See the file license.txt for copying permission. import unittest import asyncio from hbmqtt.codecs import ( bytes_to_hex_str, bytes_to_int, decode_string, encode_string, ) class TestCodecs(unittest.TestCase): def setUp(self): self.loop = asyncio.new_event_loop() def test_bytes_to_hex_str(self): ret = bytes_to_hex_str(b'\x7f') self.assertEqual(ret, '0x7f') def test_bytes_to_int(self): ret = bytes_to_int(b'\x7f') self.assertEqual(ret, 127) ret = bytes_to_int(b'\xff\xff') self.assertEqual(ret, 65535) def test_decode_string(self): stream = asyncio.StreamReader(loop=self.loop) stream.feed_data(b'\x00\x02AA') ret = self.loop.run_until_complete(decode_string(stream)) self.assertEqual(ret, 'AA') def test_encode_string(self): encoded = encode_string('AA') self.assertEqual(b'\x00\x02AA', encoded) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1579961068.0 hbmqtt-0.9.6/tox.ini0000644000175000017510000000106700000000000014250 0ustar00niconico00000000000000[flake8] ignore = E265, # block comment should start with '# ' (~64 warnings) E501, # line too long (~664 warnings) [tox] envlist = py34, py35, py36, py37, coverage, flake8, check-manifest [testenv] deps = nose commands = nosetests [testenv:flake8] deps = flake8 commands = flake8 [testenv:coverage] commands = python -m coverage erase python -m coverage run --branch --source=hbmqtt -m unittest python -m coverage report deps = coverage [testenv:check-manifest] deps = check-manifest commands = check-manifest