pax_global_header00006660000000000000000000000064144030652130014510gustar00rootroot0000000000000052 comment=7d6b2b6a2c0e94bc48e95ba9a87f190f11a3fd44 stomp.py-8.1.0/000077500000000000000000000000001440306521300133075ustar00rootroot00000000000000stomp.py-8.1.0/.gitignore000066400000000000000000000007641440306521300153060ustar00rootroot00000000000000*.py[cod] # C extensions *.so # Packages *.egg *.egg-info dist build eggs parts bin var sdist develop-eggs .installed.cfg lib lib64 __pycache__ # VIM .*.swp .*.un~ # Installer logs pip-log.txt # Unit test / coverage reports .coverage .tox nosetests.xml # Translations *.mo # Mr Developer .mr.developer.cfg .project .pydevproject .idea MANIFEST tmp/ docker/tmp docs/build .DS_Store .coverage* debian/python3-stomp.py/ debian/.debhelper debian/python3-stomp.py.* debian/files .pybuild venv stomp.py-8.1.0/.travis.yml000066400000000000000000000006771440306521300154320ustar00rootroot00000000000000sudo: required language: python python: - "3.6" - "3.7" - "3.8" - "pypy3" services: - docker env: - RABBITMQ_HOST=localhost STOMPSERVER_HOST=localhost STD_HOST=localhost STD_VHOST=/ TRAVIS=true addons: hosts: - my.example.com - my.example.org - my.example.net before_install: - pip install poetry - make docker-image - make run-docker - docker inspect stomppy - docker logs stomppy script: - make test stomp.py-8.1.0/CHANGELOG.md000066400000000000000000000526131440306521300151270ustar00rootroot00000000000000## Version 8.1.0 - Oct 2022 * Remove ssl expiration check as per PR: https://github.com/jasonrbriggs/stomp.py/pull/380 * Change deprecated PROTOCOL_TLS to PROTOCOL_TLS_CLIENT * Mock dns responses for testing: https://github.com/jasonrbriggs/stomp.py/pull/383/ * Support for connection over websocket: https://github.com/jasonrbriggs/stomp.py/pull/395 * Add default arg for try_setsockopt (https://github.com/jasonrbriggs/stomp.py/issues/391) * Updating log levels (https://github.com/jasonrbriggs/stomp.py/pull/401) * General tidy up of log messages ## Version 8.0.1 - May 2022 * Change code to use cryptography lib rather than PyOpenSSL (which is not recommended for use any more - discussed here: https://github.com/jasonrbriggs/stomp.py/issues/378) ## Version 8.0.0 - Feb 2022 * Add support for backwards compatible CONNECT in 1.1 and 1.2 protocols (https://github.com/jasonrbriggs/stomp.py/pull/348) * Flip DEFAULT_SSL_VERSION to use ssl.PROTOCOL_TLS rather than ssl.PROTOCOL_TLSv1 * Check SSL certificate for expiry if PyOpenSSL is installed * Remove deprecated constructor params (use_ssl, and other ssl params) * Minor cleanup (remove debian packaging config, since it didn't work any more) * Add log_to_stdout method to force command line logging to stdout (useful for testing) * Various updates for docker testing * Add mac keepalive functionality * Minor update to daemon attribute (https://github.com/jasonrbriggs/stomp.py/pull/361) * Fix issue with heartbeat listener disconnecting the socket (https://github.com/jasonrbriggs/stomp.py/issues/219 - https://github.com/jasonrbriggs/stomp.py/pull/369) ## Version 7.0.0 - Apr 2021 (from v6.1.1): * Add host bind port patch (https://github.com/jasonrbriggs/stomp.py/issues/331) * Tidy up based on pycharm suggestions * Change quotes to be consistent (" rather than ') (from v6.1.0): * Remove traceback logging (https://github.com/jasonrbriggs/stomp.py/pull/290) * Add support for \r\n EOL handling (as per [stomp protocol v1.2](http://stomp.github.io/stomp-specification-1.2.html#Augmented_BNF)) * Remove heartbeat loop sleep (issue https://github.com/jasonrbriggs/stomp.py/issues/297, https://github.com/jasonrbriggs/stomp.py/pull/298) * Update version number using the makefile and the poetry version command * Add `original_headers` access to the Frame so that you can get the original value of a header even if a listener modifies it (issue: https://github.com/jasonrbriggs/stomp.py/issues/300, PR https://github.com/jasonrbriggs/stomp.py/pull/309) * Fix for reconnect failures (https://github.com/jasonrbriggs/stomp.py/pull/295) * Fix for double disconnect notifications causing issues with reconnection * Add 'verbose' to stomp.logging (and defaulting the value to False). Log lines which dump the stacktrace now use that variable - except for a couple of cases (set stomp.logging.verbose = True to change back to the previous behaviour) ## Version 6.1.1 - Apr 2021 [YANKED] * Add host bind port patch (https://github.com/jasonrbriggs/stomp.py/issues/331) * Tidy up based on pycharm suggestions * Change quotes to be consistent (" rather than ') ## Version 6.1.0 - Jul 2020 [YANKED] * Remove traceback logging (https://github.com/jasonrbriggs/stomp.py/pull/290) * Add support for \r\n EOL handling (as per [stomp protocol v1.2](http://stomp.github.io/stomp-specification-1.2.html#Augmented_BNF)) * Remove heartbeat loop sleep (issue https://github.com/jasonrbriggs/stomp.py/issues/297, https://github.com/jasonrbriggs/stomp.py/pull/298) * Update version number using the makefile and the poetry version command * Add `original_headers` access to the Frame so that you can get the original value of a header even if a listener modifies it (issue: https://github.com/jasonrbriggs/stomp.py/issues/300, PR https://github.com/jasonrbriggs/stomp.py/pull/309) * Fix for reconnect failures (https://github.com/jasonrbriggs/stomp.py/pull/295) * Fix for double disconnect notifications causing issues with reconnection * Add 'verbose' to stomp.logging (and defaulting the value to False). Log lines which dump the stacktrace now use that variable - except for a couple of cases (set stomp.logging.verbose = True to change back to the previous behaviour) ## Version 6.0.0 - Feb 2020 * Update to not allow a null (None) listener when calling set_listener * Change get_ssl call in connect to be consistent with transport method * Add a sleep to the heartbeat loop * Minor change to make quote-use more consistent (replace single with double in most places) * Change build to use [Python Poetry](https://python-poetry.org/) * Test coverage improvement * Threading fix (is_alive) - https://github.com/jasonrbriggs/stomp.py/issues/286 (from v5.0.1): * Fix logging simplification code (should not be logging to root appender - https://github.com/jasonrbriggs/stomp.py/issues/275) (from v5.0.0): * Fix for credentials exposure (https://github.com/jasonrbriggs/stomp.py/pull/244) * Check for ``STOMP_SKIP_HOSTNAME_SCAN`` environment variable before extending ``LOCALHOST_NAMES`` * Remove python2 backwards compatibility * Update dockerfile for better local testing * Fix docker and travis setup, so there are consistent builds both locally and via CI * Drop deprecated start/stop methods from connection (issue https://github.com/jasonrbriggs/stomp.py/issues/257) * Fix for missing return in get_ssl (https://github.com/jasonrbriggs/stomp.py/pull/258) * Clear heartbeat event after heartbeat loop ends (https://github.com/jasonrbriggs/stomp.py/pull/260) * Update listener to move receive/message_received/heartbeat_received vars inside the 'with' blocks (https://github.com/jasonrbriggs/stomp.py/pull/252) * Simplify logging calls * Tidy up listeners (correct the behaviour of TestListener) * Fix problem with double-disconnect notification (ihttps://github.com/jasonrbriggs/stomp.py/issues/245) * Add facility for PrintingListener to write to log rather than print statements ## Version 5.0.1 - Jan 2020 [YANKED] * Fix logging simplification code (should not be logging to root appender - https://github.com/jasonrbriggs/stomp.py/issues/275) ## Version 5.0.0 - Jan 2020 [YANKED] * Fix for credentials exposure (https://github.com/jasonrbriggs/stomp.py/pull/244) * Check for ``STOMP_SKIP_HOSTNAME_SCAN`` environment variable before extending ``LOCALHOST_NAMES`` * Remove python2 backwards compatibility * Update dockerfile for better local testing * Fix docker and travis setup, so there are consistent builds both locally and via CI * Drop deprecated start/stop methods from connection (issue https://github.com/jasonrbriggs/stomp.py/issues/257) * Fix for missing return in get_ssl (https://github.com/jasonrbriggs/stomp.py/pull/258) * Clear heartbeat event after heartbeat loop ends (https://github.com/jasonrbriggs/stomp.py/pull/260) * Update listener to move receive/message_received/heartbeat_received vars inside the 'with' blocks (https://github.com/jasonrbriggs/stomp.py/pull/252) * Simplify logging calls * Tidy up listeners (correct the behaviour of TestListener) * Fix problem with double-disconnect notification (ihttps://github.com/jasonrbriggs/stomp.py/issues/245) * Add facility for PrintingListener to write to log rather than print statements ## Version 4.1.22 - Apr 2019 * Infinite retry attempts (https://github.com/jasonrbriggs/stomp.py/pull/235) * Terminate heartbeat thread on shutdown (https://github.com/jasonrbriggs/stomp.py/pull/234) * Remove unused wait_on_receipt parameter (https://github.com/jasonrbriggs/stomp.py/pull/237) * Reduce verbosity in logging to not include headers unless debug level is turned on (potential security issue as per: https://github.com/jasonrbriggs/stomp.py/issues/226) * Fix for disconnect receipt usage in transport (https://github.com/jasonrbriggs/stomp.py/issues/212) * Add __enter__/__exit__ to Connection so it can be used as a context (https://github.com/jasonrbriggs/stomp.py/issues/215) * Additional ssl options (https://github.com/jasonrbriggs/stomp.py/pull/221/) ## Version 4.1.21 - July 2018 * Fix for deadlock issue (https://github.com/jasonrbriggs/stomp.py/issues/197) * Fix for encoding issue (https://github.com/jasonrbriggs/stomp.py/issues/195) * Fix for reconnect issue (https://github.com/jasonrbriggs/stomp.py/issues/202) ## Version 4.1.20 - Feb 2018 * Tidy up API (remove need for start/stop methods on the connection) * Make updating listeners thread-safe (https://github.com/jasonrbriggs/stomp.py/pull/174) * Merge patch from Scott W for configuring grace period for heartbeat timeouts (https://github.com/jasonrbriggs/stomp.py/pull/180) * Fix ack/nack in CLI * Add missing on_receiver_loop_completed handler to ConnectionListener ## Version 4.1.19 * Replace custom script with setuptools' entry_point for creating the executable * Stop waiting for protocol answers when the connection has been closed * Add check for is_connected on transport.stop * Change command-line tool to use docopt ## Version 4.1.18 - Sep 2017 * Strip passcode from log messages ## Version 4.1.17 - Feb 2017 * Add support for password callback (https://github.com/jasonrbriggs/stomp.py/pull/140) * Add disconnect receipt handling for better notification of disconnects * Error handling for null frames * Stop raising exceptions in the receiver loop, if a connection has been disconnected ## Version 4.1.16 - Jan 2017 * bug fix for heartbeat timeout (https://github.com/jasonrbriggs/stomp.py/issues/129) * handle error with invalid/empty frames ## Version 4.1.15 - Nov 2016 * Minor change to release wheel * Note: rolled forward releases to try to fix issue 132 (https://github.com/jasonrbriggs/stomp.py/issues/132) ## Version 4.1.14 - Nov 2016 * Minor changes for ssl testing (update: now removed) ## Version 4.1.13 - Oct 2016 * Minor change to release wheel * Add proxy testing to makefile * Tidy up method parameters * Tidy up documentation * Improvement to heartbeat handling ## Version 4.1.12 - Oct 2016 * Merge patch from Nigel S, improving heartbeat accuracy (https://github.com/jasonrbriggs/stomp.py/pull/95) * Merge various patches from Ville S (including): * fixing receipt id generation (https://github.com/jasonrbriggs/stomp.py/pull/102) * generate disconnect receipt ids (https://github.com/jasonrbriggs/stomp.py/pull/108) * don't send unnecessary heartbeats (https://github.com/jasonrbriggs/stomp.py/pull/113) * fix misdetection of heartbeats (https://github.com/jasonrbriggs/stomp.py/pull/120) * Merge patch from Hugh P, adding SNI support (https://github.com/jasonrbriggs/stomp.py/pull/124) * Fix for heartbeat calculation error (https://github.com/jasonrbriggs/stomp.py/pull/125) ## Version 4.1.11 - Apr 2016 * Minor tidy up (missed from prior release) ## Version 4.1.10 - Apr 2016 * Bug fix for header escaping (https://github.com/jasonrbriggs/stomp.py/issues/82) * Merge patches from Ville S: * heartbeats - set received timestamp on receipt and error too (https://github.com/jasonrbriggs/stomp.py/pull/79) * test class name fixes (https://github.com/jasonrbriggs/stomp.py/pull/80) * support \r\n\r\n preamble end on content-length search (https://github.com/jasonrbriggs/stomp.py/pull/81) * on-demand logging message expansion (https://github.com/jasonrbriggs/stomp.py/pull/85) * bump connect error logging level to warning (https://github.com/jasonrbriggs/stomp.py/pull/87) * assign names to heartbeat and receiver threads (https://github.com/jasonrbriggs/stomp.py/pull/88) * remove unused HeartbeatListener.connected (https://github.com/jasonrbriggs/stomp.py/pull/89) * support for heartbeats on CLI (https://github.com/jasonrbriggs/stomp.py/pull/100) * Merge patch from Mikael V: * add header support in CLI (https://github.com/jasonrbriggs/stomp.py/pull/86) * Bug fix for on_before_message error (https://github.com/jasonrbriggs/stomp.py/issues/99) ## Version 4.1.9 - Jan 2016 * Merge patches from Pavel S: * support mixed string and bytes as input (https://github.com/jasonrbriggs/stomp.py/pull/66) * toggle sending of `content-length` header (https://github.com/jasonrbriggs/stomp.py/pull/67) * Minor logging change * Various documentation updates * Merge code improvement patches from Ville S: * use time.monotonic for timekeeping where available (https://github.com/jasonrbriggs/stomp.py/pull/74) * define gcd compat only where needed (https://github.com/jasonrbriggs/stomp.py/pull/75) * handle locking with "with" (https://github.com/jasonrbriggs/stomp.py/pull/76) * misc small improvements (https://github.com/jasonrbriggs/stomp.py/pull/77) * Merge patch from nigelsim to improve heartbeat handling for ActiveMQ: * heartbeat flexibility to support ActiveMQ (https://github.com/jasonrbriggs/stomp.py/pull/78) ## Version 4.1.8 - Nov 2015 * Fix missing import (https://github.com/jasonrbriggs/stomp.py/issues/61) * Code tidy up ## Version 4.1.7 - Nov 2015 * Merge patches from Ville S: * use constants more (https://github.com/jasonrbriggs/stomp.py/pull/56) * do not send headers with None values (https://github.com/jasonrbriggs/stomp.py/pull/57) * Update source to tidy up documentation * Add sphinx generated documentation * Fix keepalive bug (https://github.com/jasonrbriggs/stomp.py/issues/60) ## Version 4.1.6 - Aug 2015 * Generic exception catch on heartbeat send * Fix timeout (https://github.com/jasonrbriggs/stomp.py/issues/55) ## Version 4.1.5 - Aug 2015 * Remove incorrect \r escaping from 1.1 protocol * Merge patch from Ville S: * don't ship *.pyc (https://github.com/jasonrbriggs/stomp.py/pull/52) ## Version 4.1.4 - Aug 2015 * Add --ssl option to command line tool * Disable CTRL-C in command line tool (when in interactive mode) * Add shutdown message to cli ## Version 4.1.3 - Aug 2015 * Merge patches from Ville S: * auto-send content-length when message body is present (https://github.com/jasonrbriggs/stomp.py/pull/48) * unescape header names in addition to values (https://github.com/jasonrbriggs/stomp.py/pull/49) * remove unnecessary code (https://github.com/jasonrbriggs/stomp.py/pull/50) ## Version 4.1.2 - Jul 2015 * Merge patch from Ville S to fix coverage in setup (https://github.com/jasonrbriggs/stomp.py/pull/44) * Add Ville's change for None-check in backward3.decode (plus unit tests) ## Version 4.1.1 - Jul 2015 * Merge patches from Ville S covering invalid module references for colours in the CLI, fixing an attribute error and correctly invoking the python exe using sys.executable (https://github.com/jasonrbriggs/stomp.py/pull/41, https://github.com/jasonrbriggs/stomp.py/pull/42, https://github.com/jasonrbriggs/stomp.py/pull/43) ## Version 4.1.0 - Jul 2015 * Merge patch from George G (https://github.com/jasonrbriggs/stomp.py/pull/31) to fix binary message handling. Note that current text-only behaviour is still the default (auto_decode=True on the connection), but will be switched to false in a future release, before ultimately being removed. * Merge code cleanup patches from Ville S (https://github.com/jasonrbriggs/stomp.py/pull/35, https://github.com/jasonrbriggs/stomp.py/pull/36) * Merge another code cleanup patch from Ville E (https://github.com/jasonrbriggs/stomp.py/pull/37) ## Version 4.0.16 - Apr 2015 * Catch attribute error in SSL import (https://github.com/jasonrbriggs/stomp.py/issues/30) * Set default ssl ## Version to TLSv1 ## Version 4.0.15 - Mar 2015 * Fix for type error in transport logging (https://github.com/jasonrbriggs/stomp.py/issues/29) ## Version 4.0.14 - Mar 2015 * refactor transport to make providing new transports easier * fix bug in listener (https://github.com/jasonrbriggs/stomp.py/issues/26) * Merge Andre's logging changes * fix for issue #23 (https://github.com/jasonrbriggs/stomp.py/issues/23), stop stomp.py inserting into the path ## Version 4.0.12 - Jun 2014 * Merge Chaskiel's patch for defaulting receipt headers * Fix defaulting for host_and_ports list * Fix exception handling in receiver loop * Tidy up logging ## Version 4.0.11 - Feb 2014 * Merge Rafael's patches for specifying ssl settings as a separate method rather than constructor args - https://github.com/jasonrbriggs/stomp.py/pull/6 - https://github.com/jasonrbriggs/stomp.py/pull/10 * Fix for header escaping (as per https://github.com/jasonrbriggs/stomp.py/issues/9) * Move ip/ports for tests into setup.ini ## Version 4.0.10 - Jan 2014 * Fix package info on setup (missing adapter package causes problems for command line client - see https://github.com/jasonrbriggs/stomp.py/issues/7 for more info ## Version 4.0.9 - Jan 2014 * Fix minor issue with backward uuid func * Fixes for error number handling * Fix for message listener return values ## Version 4.0.8 - Jan 2014 * Fix return on get_listener method (https://github.com/jasonrbriggs/stomp.py/issues/4) ## Version 4.0.7 - Jan 2014 * Fix problem with heartbeat listener (https://github.com/jasonrbriggs/stomp.py/issues/4) * Add alternate aliases for the connection classes * Add initial ## Version multicast adapter (providing an interface to use the stomp.py API and send messages via multicast) - note: work in progress ## Version 4.0.6 - Dec 2013 * Fix missing headers in connect func * Throw ConnectFailedException when a connection fails in the 1.2 protocol - if wait=True is set ## Version 4.0.5 - Nov 2013 * Add command-line subscription listener. So you can do: > stomp -H localhost -P 61613 -L /queue/test * Add verbose option to command-line client (verbose "on" by default, if "off", headers aren't written to stdout) * Fix problem with connect wait (should not wait if the connection fails) * Throw exception when the connect fails (only if wait=True) * Add PrintingListener (useful for debugging) ## Version 4.0.4 - Nov 2013 * Fix ack/nack function inconsistencies in each protocol ## Version (as per https://github.com/jasonrbriggs/stomp.py/issues/1) ## Version 4.0.3 - Nov 2013 * Add script for cmd line access (so you can run `stomp -H localhost -P 61613` rather than python $PATH_TO_STOMP/stomp ....) ## Version 4.0.2 - Oct 2013 * Fix minor error with 1.2 connections. Add basic unit tests for 1.0, and 1.2 ## Version 4.0.1 - Oct 2013 * Remove the 'transform' method/functionality, as this never went into the official spec. Clients who still want this should implement themselves, using a listener (see https://github.com/jasonrbriggs/stomp.py/blob/master/stomp/test/misc_test.py for an example) * Add support for STOMP 1.2 line endings * Enforce "host" header on 1.2 connect requests (note: should be enforced on 1.1 connections, but it seems rabbitmq connections fail if host is set) ## Version 4.0.0 - Oct 2013 * Separate protocol from transport mechanism, to improve the ability to support multiple protocol versions. Note: constructor args are changing accordingly. * Add initial support for STOMP 1.2 * Moved username and passcode params out of constructor and into the connect method. The basic connection process is therefore now: > conn = stomp.Connection([('localhost', 61613)]) > conn.start() > conn.connect('admin', 'password', wait=True) ## Version 3.1.6 - Sep 2013 * Integrate fix for threading primitives issue (http://code.google.com/p/stomppy/issues/detail?id=53) * Add vhost constructor arg * Change cli to __main__ (so you can run `python stomp` rather than `python stomp/cli.py`) * Integrate interrupt patch (http://code.google.com/p/stomppy/issues/detail?id=48) * Change test hosts and ports so that they're provided from the setup.py file ## Version 3.1.5 - Aug 2013 * Fix for gcd division error (http://code.google.com/p/stomppy/issues/detail?id=44) * Fix bytes incompatibility issue in Python 3.3 (http://code.google.com/p/stomppy/issues/detail?id=51) ## Version 3.1.4 - Jul 2012 * Add receipt header to disconnect frame if not already present on a 1.1 connection ## Version 3.1.3 - May 2012 * Fix signature on override_threading method * Fix for duplicate header handling * Minor fix for ## Version var ## Version 3.1.1 - Feb 2012 * Fix for encoding problems (issue #34) [Jayson Vantuyl] * Possible fix for reconnection problems (issue #32) * Fix for broken pipe (error not passed to client - issue #33) * Various tidying up of the codebase ## Version 3.1.0 (beta 4) - Oct 2011 * Heartbeat functionality completed * General tidy up of unit tests ## Version 3.1.0 (beta 3) - Oct 2011 * Stop loading logging configuration in module itself (so stomp.py works better as an add-on library) * Fix for connection wait (so that it now actually waits) * Add initial heartbeat functionality * Add Linux TCP-Keepalive functionality, provided by Jayson Vantuyl ## Version 3.1.0 (beta 2) - Sep 2011 * Various bug fixes in 1.1 code * Fixed bug in ssl support * Added facility to override threading library * Updated unit test code for Apache Apollo ## Version 3.1.0 (beta 1) - Sep 2011 * Initial support for STOMP Protocol 1.1 * New ## Version of CLI * Added disconnect receipt functionality ## Version 3.0.4 - Sep 2011 * Added wait-for-receipt functionality * Fixed bug in CLI ## Version command * SSL protocol patch * Added connection timeout * Added facility to not send disconnect frame on disconnect (argument to disconnect function) ## Version 3.0.3 - Jan 2011 * Fixes for python 2.4 * Added config.dox to distribution ## Version 3.0.2 beta - Jun 2010 * Fix for localhost connection problem (issue #17) ## Version 3.0.1 beta - Apr 2010 * Fixes for Oracle AQ bridge for Python3 * Change to debian style changelog stomp.py-8.1.0/LICENSE000066400000000000000000000261361440306521300143240ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. stomp.py-8.1.0/MANIFEST.in000066400000000000000000000002001440306521300150350ustar00rootroot00000000000000include LICENSE include CHANGELOG include README.rst include *.conf include *.dox include stomp/test/* exclude stomp/test/*.pyc stomp.py-8.1.0/Makefile000066400000000000000000000120001440306521300147400ustar00rootroot00000000000000PYTHON:=`which python` DESTDIR=/ PROJECT=stomp.py PYTHON_VERSION_MAJOR:=$(shell $(PYTHON) -c "import sys;print(sys.version_info[0])") PLATFORM := $(shell uname) VERSION :=$(shell poetry version | sed 's/stomp.py\s*//g' | sed 's/\./, /g') SHELL=/bin/bash ARTEMIS_VERSION=2.26.0 TEST_CMD := $(shell podman network exists stomptest &> /dev/null && echo "podman unshare --rootless-netns poetry" || echo "poetry") all: test install .PHONY: docs docs: cd docs && make html updateversion: sed -i "s/__version__\s*=.*/__version__ = \($(VERSION)\)/g" stomp/__init__.py install: updateversion test poetry update poetry build poetry export -f requirements.txt --dev -o requirements.txt test: $(TEST_CMD) run pytest tests/ --cov=stomp --log-cli-level=DEBUG -v -ra --full-trace --cov-report=html:../stomppy-docs/htmlcov/ --html=tmp/report.html testsingle: $(TEST_CMD) run pytest tests/${TEST} --log-cli-level=DEBUG -v -ra --full-trace clean: rm -rf build/ MANIFEST dist/ *.egg-info/ tmp/ docker/tmp find . -name '*.pyc' -delete release: updateversion poetry build poetry publish docker/tmp/activemq-artemis-bin.tar.gz: mkdir -p docker/tmp wget http://www.apache.org/dist/activemq/activemq-artemis/${ARTEMIS_VERSION}/apache-artemis-${ARTEMIS_VERSION}-bin.tar.gz -O $@ || rm $@ ssl-setup: rm -f docker/tmp/broker* rm -f docker/tmp/expiredbroker* rm -f tmp/client* rm -f tmp/broker* rm -f tmp/expiredbroker mkdir -p docker/tmp mkdir -p tmp keytool -genkey -alias broker -keyalg RSA -keystore docker/tmp/broker.ks -storepass changeit -storetype pkcs12 -keypass changeit -dname "CN=test, OU=test, O=test, L=test, S=test, C=GB" -ext "san=ip:172.17.0.2" keytool -genkey -alias broker2 -keyalg RSA -keystore docker/tmp/broker2.ks -storepass changeit -storetype pkcs12 -keypass changeit -dname "CN=test2, OU=test2, O=test2, L=test2, S=test2, C=GB" -ext "san=ip:172.17.0.2" keytool -genkey -alias client -keyalg RSA -keystore tmp/client.ks -storepass changeit -storetype pkcs12 -keypass changeit -dname "CN=testclient, OU=testclient, O=testclient, L=testclient, S=testclient, C=GB" keytool -genkey -alias expiredbroker -keyalg RSA -keystore docker/tmp/expiredbroker.ks -storepass changeit -storetype pkcs12 -keypass changeit -dname "CN=test, OU=test, O=test, L=test, S=test, C=GB" -ext "san=ip:172.17.0.2" -startdate "2020/01/01 00:00:00" -validity 1 openssl pkcs12 -in tmp/client.ks -nodes -nocerts -out tmp/client.key -passin pass:changeit keytool -exportcert -rfc -alias client -keystore tmp/client.ks -file tmp/client.pem -storepass changeit keytool -import -alias client -keystore docker/tmp/broker.ts -file tmp/client.pem -storepass changeit -noprompt keytool -exportcert -rfc -alias broker -keystore docker/tmp/broker.ks -file tmp/broker.pem -storepass changeit keytool -exportcert -rfc -alias broker2 -keystore docker/tmp/broker2.ks -file tmp/broker2.pem -storepass changeit keytool -exportcert -rfc -alias expiredbroker -keystore docker/tmp/expiredbroker.ks -file tmp/expiredbroker.pem -storepass changeit docker-image: docker/tmp/activemq-artemis-bin.tar.gz ssl-setup docker build --build-arg ARTEMIS_VERSION=${ARTEMIS_VERSION} -t stomppy docker/ run-docker: docker run --add-host="my.example.com:127.0.0.1" --add-host="my.example.org:127.0.0.1" --add-host="my.example.net:127.0.0.1" -d -p 61613:61613 -p 62613:62613 -p 62614:62614 -p 63613:63613 -p 64613:64613 -p 15674:15674 --name stomppy -it stomppy docker ps docker exec -it stomppy /bin/sh -c "/etc/init.d/activemq start" docker exec -it stomppy /bin/sh -c "/etc/init.d/stompserver start" docker exec -it stomppy /bin/sh -c "/etc/init.d/rabbitmq-server start" docker exec -it stomppy /bin/sh -c "start-stop-daemon --start --background --exec /usr/sbin/haproxy -- -f /etc/haproxy/haproxy.cfg" docker exec -it stomppy /bin/sh -c "testbroker/bin/artemis-service start" remove-docker: docker stop stomppy docker rm stomppy docker: remove-docker docker-image run-docker podman-image: docker/tmp/activemq-artemis-bin.tar.gz ssl-setup podman build --build-arg ARTEMIS_VERSION=${ARTEMIS_VERSION} -t stomppy docker/ run-podman: podman network create --ipv6 --subnet 172.17.0.0/24 --subnet fddf:aaaa:bbbb:cccc::/64 stomptest podman run --network stomptest:ip=172.17.0.2 --add-host="my.example.com:127.0.0.1" --add-host="my.example.org:127.0.0.1" --add-host="my.example.net:127.0.0.1" -d -p 61613:61613 -p 62613:62613 -p 62614:62614 -p 63613:63613 -p 64613:64613 -p 15674:15674 --name stomppy -it stomppy podman ps podman exec -it stomppy /bin/sh -c "/etc/init.d/activemq start" podman exec -it stomppy /bin/sh -c "/etc/init.d/stompserver start" podman exec -it stomppy /bin/sh -c "/etc/init.d/rabbitmq-server start" podman exec -it stomppy /bin/sh -c "start-stop-daemon --start --background --exec /usr/sbin/haproxy -- -f /etc/haproxy/haproxy.cfg" podman exec -it stomppy /bin/sh -c "testbroker/bin/artemis-service start" remove-podman: podman stop -i stomppy podman rm -vi stomppy podman network exists stomptest && podman network rm stomptest || : podman: remove-podman podman-image run-podman stomp.py-8.1.0/README000066400000000000000000000021721440306521300141710ustar00rootroot00000000000000"stomp.py" is a Python client library for accessing messaging servers (such as ActiveMQ, Artemis or RabbitMQ) using the `STOMP protocol `_ (versions `1.0 `_, `1.1 `_ and `1.2 `_). It can also be run as a standalone, command-line client for testing. A basic example of using stomp.py can be found `here `_. More info can be found on `GitHub `_. Select: - `Version 4.2+ `_ for Python3.x only (Python2 no longer supported) - `Version 4.1.x `_ for both Python2.x and Python3.x, with support for STOMP 1.2 (note this version separates the transport mechanism from the protocol) - `Version 3.1.x `_ for both Python2.x and Python3.x (STOMP 1.0 and 1.1 only) - `Version 2.0.x `_ for Python2.xstomp.py-8.1.0/README.rst000066400000000000000000000113661440306521300150050ustar00rootroot00000000000000======== stomp.py ======== .. image:: https://badge.fury.io/py/stomp.py.svg :target: https://badge.fury.io/py/stomp.py :alt: PyPI version "stomp.py" is a Python client library for accessing messaging servers (such as ActiveMQ_, Artemis_ or RabbitMQ_) using the STOMP_ protocol (`STOMP v1.0`_, `STOMP v1.1`_ and `STOMP v1.2`_). It can also be run as a standalone, command-line client for testing. NOTE: Stomp.py has officially ended support for Python2.x. See `python3statement.org`_ for more information. .. contents:: \ :depth: 1 Quick Start =========== You can connect to a message broker running on the local machine, and send a message using the following example. .. code-block:: python import stomp conn = stomp.Connection() conn.connect('admin', 'password', wait=True) conn.send(body=' '.join(sys.argv[1:]), destination='/queue/test') conn.disconnect() Documentation and Resources =========================== - `Main documentation`_ - `API documentation`_ (see `stomp.github.io`_ for details on the STOMP protocol itself) - A basic example of using stomp.py with a message listener can be found in the `quick start`_ section of the main documentation - Description of the `command-line interface`_ - `Travis`_ for continuous integration builds - Current `test coverage report`_ - `PyPi stomp.py page`_ The current version of stomp.py supports: - Python 3.x (Python2 support ended as of Jan 2020) - STOMP version 1.0, 1.1 and 1.2 There is also legacy 3.1.7 version using the old 3-series code (see `3.1.7 on PyPi`_ and `3.1.7 on GitHub`_). This is no longer supported, but (at least as of 2018) there were still a couple of reports of this version still being used in the wild. Note: stomp.py now follows `semantic versioning`_: - MAJOR version for incompatible API changes, - MINOR version for functionality added in a backwards compatible manner, and - PATCH version for backwards compatible bug fixes. Testing ======= stomp.py has been perfunctorily tested on: - Pivotal `RabbitMQ`_ (`test_rabbitmq.py `_) - Apache `ActiveMQ`_ (`test_activemq.py `_) - Apache ActiveMQ `Artemis`_ (`test_artemis.py `_) - `stompserver`_ (`test_stompserver.py `_) For testing locally, you'll need to install docker (or `podman`_). Once installed: #. Install dependencies: ``poetry install`` #. Create the docker (or podman) image: ``make docker-image`` (or ``make podman-image``) #. Run the container: ``make run-docker`` (or ``make run-podman``) #. Run stomp.py unit tests: ``make test`` #. Cleanup the container afterwards if you don't need it any more: ``make remove-docker`` (or ``make remove-podman``) If you want to connect to the test services locally (other than from the included tests), you'll want to add test domain names to your hosts file like so: | 172.17.0.2 my.example.com | 172.17.0.2 my.example.org | 172.17.0.2 my.example.net If you're using `podman`_ and you want to access services via their private IP addresses, you'll want to run your commands with:: podman unshare --rootless-netns so that has access to the private container network. Service ports are also exposed to the host and can be accessed directly. .. _`STOMP`: http://stomp.github.io .. _`STOMP v1.0`: http://stomp.github.io/stomp-specification-1.0.html .. _`STOMP v1.1`: http://stomp.github.io/stomp-specification-1.1.html .. _`STOMP v1.2`: http://stomp.github.io/stomp-specification-1.2.html .. _`python3statement.org`: http://python3statement.org/ .. _`Main documentation`: http://jasonrbriggs.github.io/stomp.py/index.html .. _`stomp.github.io`: http://stomp.github.io/ .. _`quick start`: http://jasonrbriggs.github.io/stomp.py/quickstart.html .. _`command-line interface`: http://jasonrbriggs.github.io/stomp.py/commandline.html .. _`PyPi stomp.py page`: https://pypi.org/project/stomp.py/ .. _`API documentation`: http://jasonrbriggs.github.io/stomp.py/api.html .. _`test coverage report`: http://jasonrbriggs.github.io/stomp.py/htmlcov/ .. _`Travis`: https://travis-ci.org/jasonrbriggs/stomp.py .. _`3.1.7 on PyPi`: https://pypi.org/project/stomp.py/3.1.7/ .. _`3.1.7 on GitHub`: https://github.com/jasonrbriggs/stomp.py/tree/stomppy-3series .. _`ActiveMQ`: http://activemq.apache.org/ .. _`Artemis`: https://activemq.apache.org/components/artemis/ .. _`RabbitMQ`: http://www.rabbitmq.com .. _`stompserver`: http://stompserver.rubyforge.org .. _`semantic versioning`: https://semver.org/ .. _`podman`: https://podman.io/ stomp.py-8.1.0/docker/000077500000000000000000000000001440306521300145565ustar00rootroot00000000000000stomp.py-8.1.0/docker/Dockerfile000066400000000000000000000046221440306521300165540ustar00rootroot00000000000000FROM debian MAINTAINER Jason R Briggs EXPOSE 8484 ARG ARTEMIS_VERSION ENV ARTEMIS_VERSION $ARTEMIS_VERSION RUN apt update && apt install -y \ activemq \ haproxy \ rabbitmq-server \ rsyslog \ stompserver \ vim \ wget \ && rm -rf /var/lib/apt/lists/* # rabbitmq setup RUN rabbitmq-plugins enable rabbitmq_stomp RUN rabbitmq-plugins enable rabbitmq_web_stomp RUN echo "stomp.listeners.tcp.1 = 172.17.0.2:61613" > /etc/rabbitmq/rabbitmq.conf RUN echo "loopback_users = none" >> /etc/rabbitmq/rabbitmq.conf COPY tmp/broker.ks / COPY tmp/broker.ts / COPY tmp/expiredbroker.ks / # activemq setup RUN ln -s /etc/activemq/instances-available/main/ /etc/activemq/instances-enabled/ RUN cp -R /etc/activemq/instances-available/main/ /etc/activemq/instances-available/expiredbroker RUN ln -s /etc/activemq/instances-available/expiredbroker /etc/activemq/instances-enabled/ COPY activemq-main.xml /etc/activemq/instances-enabled/main/activemq.xml COPY activemq-expiredbroker.xml etc/activemq/instances-enabled/expiredbroker/activemq.xml ENV ACTIVEMQ_SSL_OPTS="-Djavax.net.ssl.keyStore=/broker.ks -Djavax.net.ssl.keyStorePassword=changeit -Djavax.net.ssl.trustStore=/broker.ts -Djavax.net.ssl.trustStorePassword=changeit" # stompserver setup RUN sed -i 's/port: .*/port: 63613/g' /etc/stompserver/stompserver.conf RUN sed -i 's/host: .*/host: 172.17.0.2/g' /etc/stompserver/stompserver.conf # ssl setup RUN openssl req -x509 -newkey rsa:2048 -keyout tmp/key1.pem -out tmp/cert1.pem -days 365 -nodes -subj "/CN=my.example.org" RUN openssl req -x509 -newkey rsa:2048 -keyout tmp/key2.pem -out tmp/cert2.pem -days 365 -nodes -subj "/CN=my.example.com" RUN cat tmp/cert1.pem tmp/key1.pem > tmp/myorg.pem RUN cat tmp/cert2.pem tmp/key2.pem > tmp/mycom.pem # haproxy COPY haproxy.cfg /etc/haproxy/haproxy.cfg COPY haproxy.sh / # activemq artemis COPY tmp/activemq-artemis-bin.tar.gz / RUN tar -xvzf activemq-artemis-bin.tar.gz RUN apache-artemis-${ARTEMIS_VERSION}/bin/artemis create testbroker --user testuser --password password --allow-anonymous --no-amqp-acceptor --no-hornetq-acceptor --default-port 61619 RUN sed -i 's/acceptor name="stomp">tcp:\/\/0.0.0.0:61613/acceptor name="stomp">tcp:\/\/0.0.0.0:61615/g' testbroker/etc/broker.xml # expose ports EXPOSE 61613/tcp EXPOSE 62613/tcp EXPOSE 62614/tcp EXPOSE 62619/tcp EXPOSE 63613/tcp EXPOSE 64613/tcp EXPOSE 15674/tcp ENTRYPOINT /bin/bash stomp.py-8.1.0/docker/activemq-expiredbroker.xml000066400000000000000000000021101440306521300217460ustar00rootroot00000000000000 stomp.py-8.1.0/docker/activemq-main.xml000066400000000000000000000022621440306521300200350ustar00rootroot00000000000000 stomp.py-8.1.0/docker/haproxy.cfg000066400000000000000000000007751440306521300167420ustar00rootroot00000000000000defaults mode tcp option tcplog frontend ft_test bind 0.0.0.0:65001 ssl crt /tmp/myorg.pem crt /tmp/mycom.pem no-sslv3 no-tls-tickets use_backend bk_com_cert if { ssl_fc_sni my.example.com } use_backend bk_org_cert if { ssl_fc_sni my.example.org } timeout client 1m backend bk_com_cert server srv1 172.17.0.2:62613 timeout connect 10s timeout server 1m backend bk_org_cert server srv2 172.17.0.2:62614 timeout connect 10s timeout server 1mstomp.py-8.1.0/docker/haproxy.sh000077500000000000000000000001141440306521300166030ustar00rootroot00000000000000nohup /usr/sbin/haproxy -f /etc/haproxy/haproxy.cfg > /var/log/haproxy.out &stomp.py-8.1.0/docs/000077500000000000000000000000001440306521300142375ustar00rootroot00000000000000stomp.py-8.1.0/docs/Makefile000066400000000000000000000164271440306521300157110ustar00rootroot00000000000000# Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = BUILDDIR = build HTMLBUILDDIR = ../../stomppy-docs # 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) source # the i18n builder cannot share the environment and doctrees with the others I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source .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) $(HTMLBUILDDIR) @echo @echo "Build finished. The HTML pages are in $(HTMLBUILDDIR)." 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/Stomp.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Stomp.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/Stomp" @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Stomp" @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." stomp.py-8.1.0/docs/source/000077500000000000000000000000001440306521300155375ustar00rootroot00000000000000stomp.py-8.1.0/docs/source/api.rst000066400000000000000000000235031440306521300170450ustar00rootroot00000000000000============= Using the API ============= Establishing a connection ------------------------- The simplest way to establish a connection, assuming the message broker is running on the local machine, is:: >>> import stomp >>> c = stomp.Connection([('127.0.0.1', 62613)]) >>> c.connect('admin', 'password', wait=True) By default this represents a STOMP 1.1 connection. You can request a specific version of the connection using one of the following:: >>> c = stomp.Connection10([('127.0.0.1', 62613)]) >>> c = stomp.Connection11([('127.0.0.1', 62613)]) >>> c = stomp.Connection12([('127.0.0.1', 62613)]) The first parameter to a ``Connection`` is ``host_and_ports``. This is a list of tuples, each containing ip address (which could be an ipv6 address) and the port where the message broker is listening for stomp connections. The general idea with the list is to try each address until a successful socket connection is established (giving the ability to provide multiple brokers for failover). An example of setting up a connection with failover addresses might be:: >>> import stomp >>> c = stomp.Connection([('192.168.1.100', 61613), ('192.168.1.101', 62613)]) And here's an example of an ipv6 connection:: >>> import stomp >>> c = stomp.Connection(['fe80::a00:27ff:fe90:3f1a%en1', 62613]) There are a number of other parameters for initialising the connection (looking at the StompConnection12 class): .. autoclass:: stomp.connect.StompConnection12 The final step is to call the ``connect`` method (corresponding to the `CONNECT frame `_): .. automethod:: stomp.protocol.Protocol12.connect Note that connect also allows for a map (dict) of headers to be provided, and will merge these with any additional named parameters to build the headers for the `STOMP frame `_, allowing for non-standard headers to be transmitted to the broker. Sending and receiving messages ------------------------------ Once the connection is established, you can send messages using the ``send`` method: .. automethod:: stomp.protocol.Protocol12.send To receive messages back from the messaging system, you need to setup some sort of listener on your connection, and then subscribe to the destination (see `STOMP subscribe `_). Listeners are simply a subclass which implements the methods in the ConnectionListener class (see `this page `_ for more detail). Stomp provides a few implementations of listeners, but the simplest is ``PrintingListener`` which just prints all interactions between the client and server. A simple example of this in action is:: >>> from stomp import * >>> c = Connection([('127.0.0.1', 62613)]) >>> c.set_listener('', PrintingListener()) >>> c.connect('admin', 'password', wait=True) on_connecting 127.0.0.1 62613 on_send STOMP {'passcode': 'password', 'login': 'admin', 'accept-version': '1.2', 'host': '127.0.0.1'} on_connected {'server': 'apache-apollo/1.7.1', 'host-id': 'mybroker', 'session': 'mybroker-13e0', 'heart-beat': '100,10000', 'version': '1.2', 'user-id': 'admin'} >>> c.subscribe('/queue/test', 123) on_send SUBSCRIBE {'id': 123, 'ack': 'auto', 'destination': '/queue/test'} >>> c.send('/queue/test', 'a test message') on_send SEND {'content-length': 5, 'destination': '/queue/test'} b'a test message' on_before_message {'destination': '/queue/test', 'message-id': 'mybroker-13e01', 'subscription': '123', 'ack': '2', 'content-length': '5'} a test message on_message {'destination': '/queue/test', 'message-id': 'mybroker-13e01', 'subscription': '123', 'ack': '2', 'content-length': '5'} a test message You can see the responses from the message system in the ``on_connected``, and ``on_message`` output. The stomp frames sent to the server can be seen in each ``on_send`` output (an initial STOMP connect frame, SUBSCRIBE and then SEND). In the case of the subscribe method, as of STOMP 1.1, the ``id`` parameter is required (if connecting with STOMP 1.0, only the destination is required): .. automethod:: stomp.protocol.Protocol12.subscribe Note that listeners can be named so you can use more that one type of listener at the same time:: >>> c.set_listener('stats', StatsListener()) >>> c.set_listener('print', PrintingListener()) You unsubscribe from a topic or queue using the unique ``id`` for the subscription:: >>> c.subscribe('/queue/test', 123) >>> c.unsubscribe(123) Acks and Nacks -------------- Acknowledgements are a way to tell the message server that a message was either consumed, or not. Assume a collection of clients on a server listening on a queue, and a message which requires significant processing. One of the clients receives the message, checks resource usage on the server and decides to send a nack as a consequence. The message server could, at that point, decide to send to a failover server for processing (that's a possible use, anyway). Use the client or client-individual acknowledgement parameter (see `here `_ for a description) with the subscription, in order to use acks and nacks. Afterwards, you use the message and subscription ids to ack or nack the message:: >>> conn.subscribe('/queue/test', id=4, ack='client') on_before_message {'message-id': 'mybroker-14aa2', 'destination': '/queue/test', 'subscription': '4', 'content-length': '14'} test message 1 on_message {'message-id': 'mybroker-14aa2', 'destination': '/queue/test', 'subscription': '4', 'content-length': '14'} test message 1 >>> conn.ack('mybroker-14aa2', 4) on_before_message {'message-id': 'mybroker-14ab2', 'destination': '/queue/test', 'subscription': '4', 'content-length': '14'} test message 2 on_message {'message-id': 'mybroker-14ab2', 'destination': '/queue/test', 'subscription': '4', 'content-length': '14'} test message 2 >>> conn.nack('mybroker-14ab2', 4) Transactions ------------ The STOMP protocol provides a way to transmit messages to a broker inside a transaction, which are held on the server until the transaction is either committed - at which point they're sent - or aborted - where the messages are discarded. Begin a transaction using the ``begin`` method, which returns the transaction id you then use when sending messages (you can also generate your own transaction id and pass that as a parameter to ``begin``):: >>> conn.subscribe('/queue/test', id=5) >>> txid = conn.begin() >>> conn.send('/queue/test', 'test1', transaction=txid) >>> conn.send('/queue/test', 'test2', transaction=txid) >>> conn.send('/queue/test', 'test3', transaction=txid) >>> conn.commit(txid) on_message {'subscription': '5', 'content-length': '5', 'destination': '/queue/test', 'message-id': 'mybroker-14b03', 'transaction': 'b39f3136-46a3-4e11-8ba8-845e36d48412'} test1 on_message {'subscription': '5', 'content-length': '5', 'destination': '/queue/test', 'message-id': 'mybroker-14b04', 'transaction': 'b39f3136-46a3-4e11-8ba8-845e36d48412'} test2 on_message {'subscription': '5', 'content-length': '5', 'destination': '/queue/test', 'message-id': 'mybroker-14b05', 'transaction': 'b39f3136-46a3-4e11-8ba8-845e36d48412'} test3 Abort a transaction (and discard the sent messages using ``abort``): >>> conn.subscribe('/queue/test', id=6) >>> txid = conn.begin() >>> conn.send('/queue/test', 'test4', transaction=txid) >>> conn.send('/queue/test', 'test5', transaction=txid) >>> conn.abort(txid) Disconnect ---------- Stomp.py supports graceful shutdown/disconnections through a receipt parameter (automatically generated if you don't provide it). The connection is only dropped when the server sends back a response to that receipt:: >>> conn.disconnect() on_send DISCONNECT {'receipt': '825a5cd6-9e3c-4a72-8051-72348a94f5ce'} on_receipt {'receipt-id': '825a5cd6-9e3c-4a72-8051-72348a94f5ce'} on_disconnected Dealing with disconnects ------------------------ You can use a listener to deal with connection failures, and gracefully reconnect. Consider the below 'server' code: .. code-block:: python import os import time import stomp def connect_and_subscribe(conn): conn.connect('guest', 'guest', wait=True) conn.subscribe(destination='/queue/test', id=1, ack='auto') class MyListener(stomp.ConnectionListener): def __init__(self, conn): self.conn = conn def on_error(self, frame): print('received an error "%s"' % frame.body) def on_message(self, frame): print('received a message "%s"' % frame.body) for x in range(10): print(x) time.sleep(1) print('processed message') def on_disconnected(self): print('disconnected') connect_and_subscribe(self.conn) conn = stomp.Connection([('localhost', 62613)], heartbeats=(4000, 4000)) conn.set_listener('', MyListener(conn)) connect_and_subscribe(conn) time.sleep(60) conn.disconnect() The listener in this code has an, arguably broken, message handler (on_message) which takes longer to process than the heartbeat time of 4 seconds (4000); resulting in a heartbeat timeout when a message is received, and a subsequent disconnect. The on_disconnected method then reconnects and continues processing. You can test the results of this by running the above code, and sending a message using the following 'client': .. code-block:: python import stomp conn = stomp.Connection([('localhost', 62613)]) conn.connect('guest', 'guest', wait=True) conn.send('/queue/test', 'test message') stomp.py-8.1.0/docs/source/commandline.rst000066400000000000000000000067241440306521300205700ustar00rootroot00000000000000========================================= Using the Command-line client application ========================================= Once stomp.py is installed, access the command-line client as follows:: python -m stomp -H localhost -P 61613 As of version 4.0.3, a stomp.py is also installed into the bin dir (at least on unix), so you can also run:: stomp -H localhost -P 61613 After a successful connection, you can type commands such as:: subscribe /queue/test send /queue/test hello world If you need to pass a username and password to the client:: stomp -H localhost -P 61613 -U admin -W password Run the following to see the list of startup arguments:: $ stomp --help Stomp.py command-line client Usage: stomp [options] Options: --version Show the version number and exit -h, --help Show this help message and exit -H , --host= Hostname or IP address to connect to. [default: localhost] -P , --port= Port providing stomp protocol connections. [default: 61613] -U , --user= Username for the connection -W , --password= Password for the connection -F , --file= File containing commands to be executed, instead of prompting from the command prompt. -S , --protocol= Set the STOMP protocol version (1.0, 1.1, 1.2) [default: 1.1] -L , --listen= Listen for messages on a queue/destination -V, --verbose Verbose logging "on" or "off" (if on, full headers from stomp server responses are printed) --heartbeats= Heartbeats to request when connecting with protocol >= 1.1 (two comma separated integers required) [default: 0,0] --ssl Enable SSL connection --ssl-key-file= ssl key file --ssl-cert-file= ssl cert file --ssl-ca-file= ssl ca certs file And you can also get more help within the application using the help command:: > help Documented commands (type help ): ======================================== EOF begin help rollback sendfile stats ver abort commit nack run sendrec subscribe version ack exit quit send sendreply unsubscribe Some of the differences to the programmatic API are the ability to run a script, and to send files (``run``, ``sendfile``, ``stats``):: > help run Usage: run Description: Execute commands in a specified file > help sendfile Usage: sendfile Required Parameters: destination - where to send the message filename - the file to send Description: Sends a file to a destination in the messaging system. > help stats Usage: stats [on|off] Description: Record statistics on messages sent, received, errors, etc. If no argument (on|off) is specified, dump the current statistics. Apart from that, the commands are largely inline with what you can do programmatically. Note that you can run it as a normal CLI, as a standalone listener and use it to run a script of commands. stomp.py-8.1.0/docs/source/conf.py000077500000000000000000000223141440306521300170430ustar00rootroot00000000000000#!/usr/bin/env python3 # -*- coding: utf-8 -*- # # Stomp documentation build configuration file, created by # sphinx-quickstart on Sun Sep 20 16:35:36 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 sys import os import shlex # 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('.')) sys.path.insert(0, os.path.join(os.path.abspath('.'), '..', '..')) import stomp import stomp.connect # -- 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', ] # 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 = 'Stomp' copyright = '2015, Jason R Briggs' author = 'Jason R Briggs' # 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 = '.'.join(map(str, stomp.__version__)) # The full version, including alpha/beta/rc tags. release = version + '' # 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 = ['**test**'] # 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 = 'alabaster' # 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 = 'Stompdoc' # -- 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, 'Stomp.tex', 'Stomp Documentation', 'Jason R Briggs', '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, 'stomp', 'Stomp 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, 'Stomp', 'Stomp Documentation', author, 'Stomp', '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 rst_prolog = """ .. # define a hard line break for HTML .. |br| raw:: html
"""stomp.py-8.1.0/docs/source/index.rst000066400000000000000000000023561440306521300174060ustar00rootroot00000000000000.. Stomp documentation master file, created by sphinx-quickstart on Sun Sep 20 16:35:36 2015. You can adapt this file completely to your liking, but it should at least contain the root `toctree` directive. Stomp.py |release| documentation ================================ Stomp.py was created by `Jason R Briggs `_ and contributed to by a number of fine folks (see `here `_ for more info). You can view outstanding issues, and find further info, on the `Github project page `_. Unit test coverage reports can be found `here `_. NOTE: the examples in this documentation are largely written using version 1.1 of the stomp protocol: ``_. Please check the `API documentation `_ for protocols 1.0 or 1.2, if you need to use a different version. Contents: .. toctree:: :maxdepth: 2 quickstart intro api commandline otherprojects Indices and tables ------------------ * :ref:`genindex` * :ref:`modindex` * :ref:`search` stomp.py-8.1.0/docs/source/intro.rst000066400000000000000000000042101440306521300174210ustar00rootroot00000000000000======================== Introduction to Stomp.py ======================== About Stomp.py -------------- Stomp.py started as an `"itch-scratching" project `_, after discovering that the message broker we were using for inter-application communications in a telecommunications platform, had a text-based protocol called `STOMP `_ you could use for access. We wanted something which could randomly send a variation of messages, easily scriptable - and there was only one other Python-based client library available at the time (which didn't work, and looked as if the project had stalled). So after a number of evenings spent coding (ah! those were the days - when one could get away with endless coding in the evenings), the first version of stomp.py was created (supporting the basics of the 1.0 protocol, a smidgen of a CLI, and little else). In the 8 or 9 years since its inception, support for the subsequent versions of STOMP have been added, and the command line client has been significantly enhanced. * Stomp.py currently supports all versions of the stomp protocol (1.0, 1.1 and 1.2) * The command-line client is installed via pip and has a number of useful features for testing * The code is perfunctorily tested on: ActiveMQ, RabbitMQ, stompserver, and has been reported to work with JBossMessaging in the distant past. Getting Help ------------ View outstanding issues on the GitHub `issues list `_, or raise a request for help (note that stomp.py is 'intermittently' supported at times). Contributors ------------ Contributors since moving to GitHub can be found in GitHub's `Insights page `_. Prior to that, contributors to the project included: Julian Scheid (`Rising Sun Pictures `_), Andreas Schobel, Fernando Ciciliati, Eugene Strulyov, Gavin M. Roy, Martin Pieuchot, Joe Gdaniec, Jayson Vantuyl, Tatiana Al-Chueyr Martins, Rafael Durán Casteñada, Chaskiel Grundman, and Ville Skyttä stomp.py-8.1.0/docs/source/modules.rst000066400000000000000000000000641440306521300177410ustar00rootroot00000000000000stomp ===== .. toctree:: :maxdepth: 4 stomp stomp.py-8.1.0/docs/source/otherprojects.rst000066400000000000000000000013711440306521300211660ustar00rootroot00000000000000========================= Other Projects/References ========================= This is a short, incomplete (eventually/possibly out-of-date) list of articles/projects/etc referencing stomp.py: * `Stomp.py integrated with Django `_ (July 2019) * `Using the STOMP Protocol with Apache Artemis Broker `_ (July 2018) * Stomp.py at CERN - `CERN IT Department recommended libraries `_ (October 2019). Also: `Messaging in 5 Minutes `_ (March 2011) * `RabbitMQ with SSL, STOMP, and Websockets `_ (October 2015)stomp.py-8.1.0/docs/source/quickstart.rst000066400000000000000000000043001440306521300204600ustar00rootroot00000000000000=========== Quick start =========== Stomp.py is a Python library providing access to a message broker using the `STOMP protocol `_ - either programmatically or using a command line client. Installation ============ Stomp is available on `PyPi `_. Install it in a new environment using virtualenv and pip: .. code-block:: shell $ virtualenv -p python3 teststomp $ . teststomp/bin/activate (teststomp) $ pip install stomp.py (teststomp) $ stomp --version Stomp.py API ============ A simple example of creating a listener, sending and receiving a message using localhost with the default port (61613), can be seen here:: import time import sys import stomp class MyListener(stomp.ConnectionListener): def on_error(self, frame): print('received an error "%s"' % frame.body) def on_message(self, frame): print('received a message "%s"' % frame.body) conn = stomp.Connection() conn.set_listener('', MyListener()) conn.connect('admin', 'password', wait=True) conn.subscribe(destination='/queue/test', id=1, ack='auto') conn.send(body=' '.join(sys.argv[1:]), destination='/queue/test') time.sleep(2) conn.disconnect() Assuming this was saved to stomptest.py, the code can be executed as follows: .. code-block:: shell $ python stomptest.py This is a test received a message "This is a test" Command-line Client =================== Assuming stomp.py is installed (using pip) in the site-packages directory (e.g. lib/python3.3/site-packages), hereby referred to as ${SITEPACKAGES}, then you can run the command line client as follows: .. code-block:: shell $ stomp -H localhost -P 61613 After a successful connection, you can type commands such as: .. code-block:: shell subscribe /queue/test send /queue/test hello world If you need to pass a username and password to the client: .. code-block:: shell $ stomp -H localhost -P 61613 -U admin -W password Type help for more information once you're running the command-line interface, or run the following to see the list of startup arguments: .. code-block:: shell $ stomp --help stomp.py-8.1.0/docs/source/stomp.adapter.rst000066400000000000000000000005361440306521300210560ustar00rootroot00000000000000stomp.adapter package ===================== Submodules ---------- stomp.adapter.multicast module ------------------------------ .. automodule:: stomp.adapter.multicast :members: :undoc-members: :show-inheritance: Module contents --------------- .. automodule:: stomp.adapter :members: :undoc-members: :show-inheritance: stomp.py-8.1.0/docs/source/stomp.rst000066400000000000000000000025221440306521300174340ustar00rootroot00000000000000stomp package ============= .. # define a hard line break for HTML .. |br| raw:: html Subpackages ----------- .. toctree:: stomp.adapter stomp.test Submodules ---------- stomp.colours module ------------------- .. automodule:: stomp.colours :members: :undoc-members: :show-inheritance: stomp.connect module -------------------- .. automodule:: stomp.connect :members: :undoc-members: :show-inheritance: stomp.constants module ---------------------- .. automodule:: stomp.constants :members: :undoc-members: :show-inheritance: stomp.exception module ---------------------- .. automodule:: stomp.exception :members: :undoc-members: :show-inheritance: stomp.listener module --------------------- .. automodule:: stomp.listener :members: :undoc-members: :show-inheritance: stomp.protocol module --------------------- .. automodule:: stomp.protocol :members: :undoc-members: :show-inheritance: stomp.transport module ---------------------- .. automodule:: stomp.transport :members: :undoc-members: :show-inheritance: stomp.utils module ------------------ .. automodule:: stomp.utils :members: :undoc-members: :show-inheritance: Module contents --------------- .. automodule:: stomp :members: :undoc-members: :show-inheritance: stomp.py-8.1.0/docs/source/stomp.test.rst000066400000000000000000000052021440306521300204100ustar00rootroot00000000000000stomp.test package ================== Submodules ---------- stomp.test.basic_test module ---------------------------- .. automodule:: stomp.test.basic_test :members: :undoc-members: :show-inheritance: stomp.test.cli_test module -------------------------- .. automodule:: stomp.test.cli_test :members: :undoc-members: :show-inheritance: stomp.test.misc_test module --------------------------- .. automodule:: stomp.test.misc_test :members: :undoc-members: :show-inheritance: stomp.test.multicast_test module -------------------------------- .. automodule:: stomp.test.multicast_test :members: :undoc-members: :show-inheritance: stomp.test.nonascii_test module ---------------------------------- .. automodule:: stomp.test.nonascii_test :members: :undoc-members: :show-inheritance: stomp.test.rabbitmq_test module ------------------------------- .. automodule:: stomp.test.rabbitmq_test :members: :undoc-members: :show-inheritance: stomp.test.s10_test module -------------------------- .. automodule:: stomp.test.s10_test :members: :undoc-members: :show-inheritance: stomp.test.s11_test module -------------------------- .. automodule:: stomp.test.s11_test :members: :undoc-members: :show-inheritance: stomp.test.s12_test module -------------------------- .. automodule:: stomp.test.s12_test :members: :undoc-members: :show-inheritance: stomp.test.ss_test module ------------------------- .. automodule:: stomp.test.ss_test :members: :undoc-members: :show-inheritance: stomp.test.ssl_test module -------------------------- .. automodule:: stomp.test.ssl_test :members: :undoc-members: :show-inheritance: stomp.test.stompserver_test module ---------------------------------- .. automodule:: stomp.test.stompserver_test :members: :undoc-members: :show-inheritance: stomp.test.testutils module --------------------------- .. automodule:: stomp.test.testutils :members: :undoc-members: :show-inheritance: stomp.test.threading_test module -------------------------------- .. automodule:: stomp.test.threading_test :members: :undoc-members: :show-inheritance: stomp.test.transport_test module -------------------------------- .. automodule:: stomp.test.transport_test :members: :undoc-members: :show-inheritance: stomp.test.utils_test module ---------------------------- .. automodule:: stomp.test.utils_test :members: :undoc-members: :show-inheritance: Module contents --------------- .. automodule:: stomp.test :members: :undoc-members: :show-inheritance: stomp.py-8.1.0/poetry.lock000066400000000000000000000160221440306521300155040ustar00rootroot00000000000000[[package]] name = "atomicwrites" version = "1.4.1" description = "Atomic file writes." category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" marker = "sys_platform == \"win32\"" [[package]] name = "attrs" version = "22.1.0" description = "Classes Without Boilerplate" category = "dev" optional = false python-versions = ">=3.5" [package.extras] dev = ["coverage (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "mypy (>=0.900,<0.940 || >0.940)", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit", "cloudpickle"] docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"] tests = ["coverage (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "mypy (>=0.900,<0.940 || >0.940)", "pytest-mypy-plugins", "zope.interface", "cloudpickle"] tests_no_zope = ["coverage (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "mypy (>=0.900,<0.940 || >0.940)", "pytest-mypy-plugins", "cloudpickle"] [[package]] name = "colorama" version = "0.4.5" description = "Cross-platform colored terminal text." category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" marker = "sys_platform == \"win32\"" [[package]] name = "coverage" version = "6.2" description = "Code coverage measurement for Python" category = "dev" optional = false python-versions = ">=3.6" [package.extras] toml = ["tomli"] [package.dependencies] [package.dependencies.tomli] version = "*" optional = true [[package]] name = "docopt" version = "0.6.2" description = "Pythonic argument parser, that will make you smile" category = "main" optional = false python-versions = "*" [[package]] name = "importlib-metadata" version = "4.8.3" description = "Read metadata from Python packages" category = "dev" optional = false python-versions = ">=3.6" marker = "python_version < \"3.8\"" [package.extras] docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] perf = ["ipython"] testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "packaging", "pep517", "pyfakefs", "flufl.flake8", "pytest-perf (>=0.9.2)", "pytest-black (>=0.3.7)", "pytest-mypy", "importlib-resources (>=1.3)"] [package.dependencies] zipp = ">=0.5" [package.dependencies.typing-extensions] version = ">=3.6.4" python = "<3.8" [[package]] name = "iniconfig" version = "1.1.1" description = "iniconfig: brain-dead simple config-ini parsing" category = "dev" optional = false python-versions = "*" [[package]] name = "packaging" version = "21.3" description = "Core utilities for Python packages" category = "dev" optional = false python-versions = ">=3.6" [package.dependencies] pyparsing = ">=2.0.2,<3.0.5 || >3.0.5" [[package]] name = "pluggy" version = "1.0.0" description = "plugin and hook calling mechanisms for python" category = "dev" optional = false python-versions = ">=3.6" [package.extras] testing = ["pytest-benchmark", "pytest"] dev = ["tox", "pre-commit"] [package.dependencies] [package.dependencies.importlib-metadata] version = ">=0.12" python = "<3.8" [[package]] name = "py" version = "1.11.0" description = "library with cross-python path, ini-parsing, io, code, log facilities" category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" [[package]] name = "pyparsing" version = "3.0.7" description = "Python parsing module" category = "dev" optional = false python-versions = ">=3.6" [package.extras] diagrams = ["jinja2", "railroad-diagrams"] [[package]] name = "pytest" version = "7.0.1" description = "pytest: simple powerful testing with Python" category = "dev" optional = false python-versions = ">=3.6" [package.extras] testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"] [package.dependencies] atomicwrites = ">=1.0" attrs = ">=19.2.0" colorama = "*" iniconfig = "*" packaging = "*" pluggy = ">=0.12,<2.0" py = ">=1.8.2" tomli = ">=1.0.0" [package.dependencies.importlib-metadata] version = ">=0.12" python = "<3.8" [[package]] name = "pytest-cov" version = "3.0.0" description = "Pytest plugin for measuring coverage." category = "dev" optional = false python-versions = ">=3.6" [package.extras] testing = ["virtualenv", "pytest-xdist", "six", "process-tests", "hunter", "fields"] [package.dependencies] pytest = ">=4.6" [package.dependencies.coverage] version = ">=5.2.1" extras = ["toml"] [[package]] name = "pytest-html" version = "3.1.1" description = "pytest plugin for generating HTML reports" category = "dev" optional = false python-versions = ">=3.6" [package.dependencies] pytest = ">=5.0,<6.0.0 || >6.0.0" pytest-metadata = "*" [[package]] name = "pytest-metadata" version = "1.11.0" description = "pytest plugin for test session metadata" category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" [package.dependencies] pytest = ">=2.9.0" [[package]] name = "pytest-mock" version = "3.6.1" description = "Thin-wrapper around the mock package for easier use with pytest" category = "dev" optional = false python-versions = ">=3.6" [package.extras] dev = ["pre-commit", "tox", "pytest-asyncio"] [package.dependencies] pytest = ">=5.0" [[package]] name = "pytest-ordering" version = "0.6" description = "pytest plugin to run your tests in a specific order" category = "dev" optional = false python-versions = "*" [package.dependencies] pytest = "*" [[package]] name = "tomli" version = "1.2.3" description = "A lil' TOML parser" category = "dev" optional = false python-versions = ">=3.6" [[package]] name = "typing-extensions" version = "4.1.1" description = "Backported and Experimental Type Hints for Python 3.6+" category = "dev" optional = false python-versions = ">=3.6" marker = "python_version < \"3.8\"" [[package]] name = "websocket-client" version = "1.3.1" description = "WebSocket client for Python with low level API options" category = "main" optional = false python-versions = ">=3.6" [package.extras] test = ["websockets"] optional = ["wsaccel", "python-socks"] docs = ["sphinx-rtd-theme (>=0.5)", "Sphinx (>=3.4)"] [[package]] name = "zipp" version = "3.6.0" description = "Backport of pathlib-compatible object wrapper for zip files" category = "dev" optional = false python-versions = ">=3.6" marker = "python_version < \"3.8\"" [package.extras] docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy"] [metadata] python-versions = "^3.6" content-hash = "48ffba54b47d04f160b0572c4bf1192209eb64b6c4361dd94536698b51fd313e" [metadata.files] atomicwrites = [] attrs = [] colorama = [] coverage = [] docopt = [] importlib-metadata = [] iniconfig = [] packaging = [] pluggy = [] py = [] pyparsing = [] pytest = [] pytest-cov = [] pytest-html = [] pytest-metadata = [] pytest-mock = [] pytest-ordering = [] tomli = [] typing-extensions = [] websocket-client = [] zipp = [] stomp.py-8.1.0/pyproject.toml000066400000000000000000000017201440306521300162230ustar00rootroot00000000000000[tool.poetry] name = "stomp.py" version = "8.1.0" description = "Python STOMP client, supporting versions 1.0, 1.1 and 1.2 of the protocol" authors = ["Jason R Briggs "] license = "Apache-2.0" readme = "README.rst" repository = "https://github.com/jasonrbriggs/stomp.py" documentation = "http://jasonrbriggs.github.io/stomp.py/" keywords = ["stomp", "messaging", "events", "client"] classifiers = [ "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", ] packages = [ { include = "stomp" } ] [tool.poetry.dependencies] python = "^3.6" docopt = "^0.6.2" websocket-client = "^1.2.3" [tool.poetry.dev-dependencies] pytest = ">=5.2" pytest-cov = ">=2.8.1" pytest-mock = ">=2.0.0" pytest-html = ">=2.0.1" pytest-ordering = ">=0.6" [tool.poetry.scripts] stomp = "stomp.__main__:main" [build-system] requires = ["poetry>=0.12"] build-backend = "poetry.masonry.api" stomp.py-8.1.0/pytest.ini000066400000000000000000000000631440306521300153370ustar00rootroot00000000000000[pytest] filterwarnings = ignore:.*TestListener.*:stomp.py-8.1.0/requirements.txt000066400000000000000000000007521440306521300165770ustar00rootroot00000000000000atomicwrites==1.4.1; sys_platform == "win32" attrs==22.1.0 colorama==0.4.5; sys_platform == "win32" coverage==6.2 docopt==0.6.2 importlib-metadata==4.8.3; python_version < "3.8" iniconfig==1.1.1 packaging==21.3 pluggy==1.0.0 py==1.11.0 pyparsing==3.0.7 pytest==7.0.1 pytest-cov==3.0.0 pytest-html==3.1.1 pytest-metadata==1.11.0 pytest-mock==3.6.1 pytest-ordering==0.6 tomli==1.2.3 typing-extensions==4.1.1; python_version < "3.8" websocket-client==1.3.1 zipp==3.6.0; python_version < "3.8" stomp.py-8.1.0/setup.cfg000066400000000000000000000002031440306521300151230ustar00rootroot00000000000000[bdist_wheel] universal = 1 [flake8] ignore = E265,E501,F401,F403,F405,H301,H404,H405 import-order-style = google exclude = build stomp.py-8.1.0/stomp/000077500000000000000000000000001440306521300144515ustar00rootroot00000000000000stomp.py-8.1.0/stomp/__init__.py000066400000000000000000000024721440306521300165670ustar00rootroot00000000000000"""stomp.py provides connectivity to a message broker supporting the STOMP protocol. Protocol versions 1.0, 1.1 and 1.2 are supported. See the GITHUB project page for more information. Author: Jason R Briggs |br| License: http://www.apache.org/licenses/LICENSE-2.0 |br| Project Page: https://github.com/jasonrbriggs/stomp.py """ import stomp.adapter as adapter import stomp.connect as connect import stomp.listener as listener import stomp.logging as logging __version__ = (8, 1, 0) ## # Alias for STOMP 1.0 connections. # Connection10 = connect.StompConnection10 StompConnection10 = Connection10 ## # Alias for STOMP 1.1 connections. # Connection11 = connect.StompConnection11 StompConnection11 = Connection11 ## # Alias for STOMP 1.2 connections. # Connection12 = connect.StompConnection12 StompConnection12 = Connection12 WSConnection = adapter.ws.WSStompConnection WSStompConnection = WSConnection ## # Default connection alias (STOMP 1.1). # Connection = connect.StompConnection11 ## # Access to the default connection listener. # ConnectionListener = listener.ConnectionListener ## # Access to the stats listener. # StatsListener = listener.StatsListener ## # Access to the 'waiting' listener. WaitingListener = listener.WaitingListener ## # Access to the printing listener PrintingListener = listener.PrintingListener stomp.py-8.1.0/stomp/__main__.py000066400000000000000000000505651440306521300165560ustar00rootroot00000000000000""" Stomp.py command-line client Usage: stomp [options] Options: --version Show the version number and exit -h, --help Show this help message and exit -H , --host= Hostname or IP address to connect to. [default: localhost] -P , --port= Port providing stomp protocol connections. [default: 61613] -U , --user= Username for the connection -W , --password= Password for the connection -F , --file= File containing commands to be executed, instead of prompting from the command prompt. -S , --protocol= Set the STOMP protocol version (1.0, 1.1, 1.2) [default: 1.1] -L , --listen= Listen for messages on a queue/destination -V, --verbose Verbose logging "on" or "off" (if on, full headers from stomp server responses are printed) --heartbeats= Heartbeats to request when connecting with protocol >= 1.1 (two comma separated integers required) [default: 0,0] --ssl Enable SSL connection --ssl-key-file= ssl key file --ssl-cert-file= ssl cert file --ssl-ca-file= ssl ca certs file """ import base64 import json import os import re import sys import time from cmd import Cmd from functools import partial from docopt import docopt sys.path.append(os.path.dirname(os.path.dirname(os.path.realpath(__file__)))) import stomp.colours import stomp.utils from stomp.adapter.multicast import MulticastConnection from stomp.connect import StompConnection10, StompConnection11, StompConnection12 from stomp.listener import ConnectionListener, StatsListener sys.path.append(".") import stomp version_string = "%s.%s.%s" % stomp.__version__ heartbeat_pattern = re.compile(r"[0-9]+,[0-9]+") class SubscriptionInfo(object): """ Used to store info about a subscription. """ def __init__(self, id, ack): self.id = id self.ack = ack class StompCLI(Cmd, ConnectionListener): """ A command line interface to the stomp.py client. See :py:class:`stomp.connect.StompConnection11` for more information on establishing a connection to a stomp server. """ def __init__(self, host="localhost", port=61613, user="", passcode="", ver="1.1", prompt="> ", verbose=True, heartbeats=(0, 0), use_ssl=False, ssl_key_file=None, ssl_cert_file=None, ssl_ca_file=None, stdin=sys.stdin, stdout=sys.stdout): Cmd.__init__(self, "Tab", stdin, stdout) ConnectionListener.__init__(self) self.__start = True self.prompt = prompt self.verbose = verbose self.user = user self.passcode = passcode self.__quit = False if ver == "1.0": self.conn = StompConnection10([(host, port)]) elif ver == "1.1": self.conn = StompConnection11([(host, port)], heartbeats=heartbeats) elif ver == "1.2": self.conn = StompConnection12([(host, port)], heartbeats=heartbeats) elif ver == "multicast": self.conn = MulticastConnection() else: raise RuntimeError("Unknown version") if use_ssl: self.conn.set_ssl([(host, port)], key_file=ssl_key_file, cert_file=ssl_cert_file, ca_certs=ssl_ca_file) self.conn.set_listener("", self) self.conn.connect(self.user, self.passcode, wait=True) self.transaction_id = None self.version = ver try: self.nversion = float(ver) except ValueError: self.nversion = 1.0 self.__subscriptions = {} self.__subscription_id = 1 def __print_async(self, frame_type, frame): """ Utility function to print a message and setup the command prompt for the next input """ if self.__quit: return if self.verbose: self.__sysout(frame_type) for k, v in frame.headers.items(): self.__sysout("%s: %s" % (k, v)) else: if "message-id" in frame.headers: self.__sysout("message-id: %s" % frame.headers["message-id"]) if "subscription" in frame.headers: self.__sysout("subscription: %s" % frame.headers["subscription"]) if self.prompt != "": self.__sysout("") self.__sysout(frame.body) if not self.__start: self.__sysout(self.prompt, end="") else: self.__start = False self.stdout.flush() def __sysout(self, msg, end="\n"): if self.__quit: return self.stdout.write(str(msg) + end) def __error(self, msg, end="\n"): self.stdout.write(stomp.colours.BOLD + stomp.colours.RED + str(msg) + stomp.colours.NO_COLOUR + end) def on_connecting(self, host_and_port): """ See :py:meth:`ConnectionListener.on_connecting` """ pass def on_disconnected(self): """ see :py:meth:`ConnectionListener.on_disconnected` """ if not self.__quit: self.__error("lost connection") def on_message(self, frame): """ See :py:meth:`ConnectionListener.on_message` Special case: if the header 'filename' is present, the content is written out as a file """ self.__sysout("") if "filename" in frame.headers: content = base64.b64decode(frame.body.encode()) if os.path.exists(frame.headers["filename"]): fname = "%s.%s" % (frame.headers["filename"], int(time.time())) else: fname = frame.headers["filename"] with open(fname, "wb") as f: f.write(content) frame.body = "Saved file: %s" % fname self.__print_async("MESSAGE", frame) def on_error(self, frame): """ See :py:meth:`ConnectionListener.on_error` """ self.__print_async("ERROR", frame) def on_receipt(self, frame): """ See :py:meth:`ConnectionListener.on_receipt` """ self.__print_async("RECEIPT", frame) def on_connected(self, frame): """ See :py:meth:`ConnectionListener.on_connected` """ self.__print_async("CONNECTED", frame) def on_send(self, frame): if self.verbose: self.__sysout("Sending %s" % str(frame)) def help_help(self): self.__sysout("Quick help on commands") def default(self, line): self.__error("Unknown command: %s" % line.split()[0]) def emptyline(self): pass def help(self, usage, description, required=(), optional=()): rparams = "\n\t" + "\n\t".join(required) oparams = "\n\t" + "\n\t".join(optional) m = { "hl": stomp.colours.BOLD + stomp.colours.GREEN, "nc": stomp.colours.NO_COLOUR, "usage": usage, "description": description, "required": rparams.rstrip(), "optional": oparams.rstrip() } if rparams.rstrip() != "": rparams = '''%(hl)sRequired Parameters:%(nc)s%(required)s\n\n''' % m m["required"] = rparams if oparams.rstrip() != "": oparams = '''%(hl)sOptional Parameters:%(nc)s%(optional)s\n\n''' % m m["optional"] = oparams self.__sysout('''%(hl)sUsage:%(nc)s \t%(usage)s %(required)s%(optional)s%(hl)sDescription:%(nc)s \t%(description)s ''' % m) def do_quit(self, args): self.__quit = True self.__sysout("Shutting down, please wait") return True do_exit = do_quit do_EOF = do_quit def help_quit(self): self.help("exit", "Exit the stomp client") help_exit = help_quit def help_EOF(self): self.help("exit", "Exit the stomp client (using CTRL-D)") def do_subscribe(self, args): args = args.split() if len(args) < 1: self.__error("Expecting: subscribe [ack]") return name = args[0] if name in self.__subscriptions: self.__error("Already subscribed to %s" % name) return ack_mode = "auto" if len(args) >= 2: ack_mode = args[1] sid = self.__subscription_id self.__subscription_id += 1 self.__sysout("Subscribing to '%s' with acknowledge set to '%s', id set to '%s'" % (name, ack_mode, sid)) self.conn.subscribe(destination=name, ack=ack_mode, id=sid) self.__subscriptions[name] = SubscriptionInfo(sid, ack_mode) def help_subscribe(self): self.help("subscribe [ack]", '''Register to listen to a given destination. Like send, the subscribe command requires a destination \theader indicating which destination to subscribe to. The ack parameter is optional, and defaults to \tauto.''', ["destination - the name to subscribe to"], ["ack - how to handle acknowledgements for a message; either automatically (auto) or manually (client)"]) def do_unsubscribe(self, args): args = args.split() if len(args) < 1: self.__error("Expecting: unsubscribe ") return if args[0] not in self.__subscriptions: self.__sysout("Subscription %s not found" % args[0]) return self.__sysout("Unsubscribing from '%s'" % args[0]) self.conn.unsubscribe(destination=args[0], id=self.__subscriptions[args[0]].id) del self.__subscriptions[args[0]] def help_unsubscribe(self): self.help("unsubscribe ", "Remove an existing subscription - so that the client no longer receive messages from that destination.", ["destination - the name to unsubscribe from"], ["ack - how to handle acknowledgements for a message; either automatically (auto) or manually (client)"]) def do_send(self, args): args = args.split() if len(args) < 2: self.__error("Expecting: send ") elif not self.transaction_id: self.conn.send(args[0], " ".join(args[1:])) else: self.conn.send(args[0], " ".join(args[1:]), transaction=self.transaction_id) def complete_send(self, text, line): mline = line.split(" ")[1] offs = len(mline) - len(text) return [s[offs:] for s in self.__subscriptions if s.startswith(mline)] complete_unsubscribe = complete_send complete_sendrec = complete_send complete_sendreply = complete_send complete_sendfile = complete_send def help_send(self): self.help("send ", "Sends a message to a destination in the messaging system.", ["destination - where to send the message", "message - the content to send"]) def do_sendrec(self, args): args = args.split() receipt_id = stomp.utils.get_uuid() if len(args) < 2: self.__error("Expecting: sendrec ") elif not self.transaction_id: self.conn.send(args[0], " ".join(args[1:]), receipt=receipt_id) else: self.conn.send(args[0], " ".join(args[1:]), transaction=self.transaction_id, receipt=receipt_id) def help_sendrec(self): self.help("sendrec ", "Sends a message to a destination in the messaging system and blocks for receipt of the message.", ["destination - where to send the message", "message - the content to send"]) def do_sendreply(self, args): args = args.split() if len(args) < 3: self.__error("Expecting: sendreply ") else: self.conn.send(args[0], "%s\n" % " ".join(args[2:]), headers={"correlation-id": args[1]}) def help_sendreply(self): self.help("sendreply ", "Sends a reply message to a destination in the messaging system.", ["destination - where to send the message", "correlation-id - the correlating identifier to send with the response", "message - the content to send"]) def do_sendfile(self, args): args = args.split() if len(args) < 2: self.__error("Expecting: sendfile [headers.json]") elif not os.path.exists(args[1]): self.__error("File %s does not exist" % args[1]) else: headers = {} if len(args) == 3: if not os.path.exists(args[2]): self.__error("File %s does not exist" % args[2]) return self.__sysout("Loading %s" % args[2]) with open(args[2], mode="rb") as jf: headers = json.load(jf) self.__sysout("Using headers %s" % str(headers)) with open(args[1], mode="rb") as f: s = f.read() msg = base64.b64encode(s).decode() if not self.transaction_id: self.conn.send(args[0], msg, filename=args[1], headers=headers) else: self.conn.send(args[0], msg, filename=args[1], headers=headers, transaction=self.transaction_id) def help_sendfile(self): self.help("sendfile [headers.json]", "Sends a file to a destination in the messaging system.", ["destination - where to send the message", "filename - the file to send", "headers.json - json map with headers to send"]) def do_version(self, args): self.__sysout("%s%s [Protocol version %s]%s" % (stomp.colours.BOLD, version_string, self.conn.version, stomp.colours.NO_COLOUR)) do_ver = do_version def help_version(self): self.help("version", "Display the version of the client") help_ver = help_version def check_ack_nack(self, acknackfunc, args): if self.nversion >= 1.2 and len(args) < 1: self.__error("Expecting: %s " % acknackfunc) return None elif self.nversion == 1.1 and len(args) < 2: self.__error("Expecting: %s " % acknackfunc) return None elif len(args) < 1: self.__error("Expecting: %s " % acknackfunc) return None if self.nversion == 1.1: return partial(acknackfunc, args[0], args[1]) else: return partial(acknackfunc, args[0]) def do_ack(self, args): args = args.split() func = self.check_ack_nack(self.conn.ack, args) if func is None: return if not self.transaction_id: func() else: func(transaction=self.transaction_id) def help_ack(self): self.help("ack [subscription-id]", '''The command 'ack' is used to acknowledge consumption of a message from a subscription using client \tacknowledgment. When a client has issued a 'subscribe' with the ack flag set to client, any messages \treceived from that destination will not be considered to have been consumed (by the server) until \tthe message has been acknowledged.''', ["message-id - the id of the message being acknowledged"], ["subscription-id the id of the subscription (only required for STOMP 1.1)"]) def do_nack(self, args): args = args.split() func = self.check_ack_nack(self.conn.nack, args) if func is None: return if not self.transaction_id: func() else: func(transaction=self.transaction_id) def help_nack(self): self.help("nack [subscription]", '''The command 'nack' is used to acknowledge the failure of a message from a subscription using client \tacknowledgment. When a client has issued a 'subscribe' with the ack flag set to client, any messages \treceived from that destination will not be considered to have been consumed (by the server) until \tthe message has been acknowledged (ack or nack).''', ["message-id - the id of the message being acknowledged"]) def do_abort(self, args): if not self.transaction_id: self.__error("Not currently in a transaction") else: self.conn.abort(transaction=self.transaction_id) self.__sysout("Aborted transaction: %s" % self.transaction_id) self.transaction_id = None do_rollback = do_abort def help_abort(self): self.help("abort", "Roll back a transaction in progress.") help_rollback = help_abort def do_begin(self, args): if self.transaction_id: self.__error("Currently in a transaction (%s)" % self.transaction_id) else: self.transaction_id = self.conn.begin() self.__sysout("Transaction id: %s" % self.transaction_id) def help_begin(self): self.help("begin", '''Start a transaction. Transactions in this case apply to sending and acknowledging - \tany messages sent or acknowledged during a transaction will be handled atomically based on the \ttransaction.''') def do_commit(self, args): if not self.transaction_id: self.__error("Not currently in a transaction") else: self.__sysout("Committing %s" % self.transaction_id) self.conn.commit(transaction=self.transaction_id) self.transaction_id = None def help_commit(self): self.help("commit", "Commit a transaction in progress.") def do_stats(self, args): args = args.split() if len(args) < 1: stats = self.conn.get_listener("stats") if stats: self.__sysout(stats) else: self.__error("No stats available") elif args[0] == "on": self.conn.set_listener("stats", StatsListener()) elif args[0] == "off": self.conn.remove_listener("stats") else: self.__error("Expecting: stats [on|off]") def help_stats(self): self.help("stats [on|off]", '''Record statistics on messages sent, received, errors, etc. If no argument (on|off) is specified, \tdump the current statistics.''') def do_run(self, args): args = args.split() if len(args) == 0: self.__error("Expecting: run ") elif not os.path.exists(args[0]): self.__error("File %s was not found" % args[0]) else: with open(args[0]) as f: lines = f.read().split("\n") for line in lines: self.onecmd(line) def help_run(self): self.help("run ", "Execute commands in a specified file") def do_nothing_loop(): while 1: time.sleep(1) def main(): arguments = docopt(__doc__, version=version_string) if arguments["--listen"] is not None: prompt = "" else: prompt = "> " if not heartbeat_pattern.match(arguments["--heartbeats"]): print("Invalid heartbeats, expecting cx,cy") sys.exit(1) heartbeats = tuple(map(int, arguments["--heartbeats"].split(","))) st = StompCLI(arguments["--host"], arguments["--port"], arguments["--user"], arguments["--password"], arguments["--protocol"], prompt, arguments["--verbose"], heartbeats=heartbeats, use_ssl=arguments["--ssl"], ssl_key_file=arguments["--ssl-key-file"], ssl_cert_file=arguments["--ssl-cert-file"], ssl_ca_file=arguments["--ssl-ca-file"]) if arguments["--listen"] is not None: st.do_subscribe(arguments["--listen"]) try: while 1: time.sleep(10) except: print("\n") elif arguments["--file"] is not None: st.do_run(arguments["--file"]) else: # disable CTRL-C, since can't guarantee correct handling of disconnect import signal def signal_handler(signal, frame): pass signal.signal(signal.SIGINT, signal_handler) try: try: st.cmdloop() except KeyboardInterrupt: st.do_quit() finally: st.conn.disconnect() # # command line access # if __name__ == "__main__": try: main() except: pass stomp.py-8.1.0/stomp/adapter/000077500000000000000000000000001440306521300160715ustar00rootroot00000000000000stomp.py-8.1.0/stomp/adapter/__init__.py000066400000000000000000000001261440306521300202010ustar00rootroot00000000000000"""Non-standard adapters. """ import stomp.adapter.multicast import stomp.adapter.ws stomp.py-8.1.0/stomp/adapter/multicast.py000066400000000000000000000137771440306521300204670ustar00rootroot00000000000000"""Multicast transport for stomp.py. Obviously not a typical message broker, but convenient if you don't have a broker, but still want to use stomp.py methods. """ import struct from stomp.connect import BaseConnection from stomp.protocol import * from stomp.transport import * from stomp.utils import * MCAST_GRP = "224.1.1.1" MCAST_PORT = 5000 class MulticastTransport(Transport): """ Transport over multicast connections rather than using a broker. """ def __init__(self, encoding): Transport.__init__(self, [], False, False, 0.0, 0.0, 0.0, 0.0, 0, None, None, None, False, encoding) self.subscriptions = {} self.current_host_and_port = (MCAST_GRP, MCAST_PORT) def attempt_connection(self): """ Establish a multicast connection - uses 2 sockets (one for sending, the other for receiving) """ self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP) self.socket.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, 2) self.receiver_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP) self.receiver_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) self.receiver_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1) self.receiver_socket.bind(("", MCAST_PORT)) mreq = struct.pack("4sl", socket.inet_aton(MCAST_GRP), socket.INADDR_ANY) self.receiver_socket.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, mreq) if not self.socket or not self.receiver_socket: raise exception.ConnectFailedException() def send(self, encoded_frame): """ Send an encoded frame through the mcast socket. :param bytes encoded_frame: """ self.socket.sendto(encoded_frame, (MCAST_GRP, MCAST_PORT)) def receive(self): """ Receive 1024 bytes from the multicast receiver socket. :rtype: bytes """ return self.receiver_socket.recv(1024) def process_frame(self, f, frame_str): """ :param Frame f: Frame object :param bytes frame_str: Raw frame content """ frame_type = f.cmd.lower() if frame_type in ["disconnect"]: return if frame_type == "send": frame_type = "message" f.cmd = "MESSAGE" if frame_type in ["connected", "message", "receipt", "error", "heartbeat"]: if frame_type == "message": if f.headers["destination"] not in self.subscriptions.values(): return self.notify("before_message", f) self.notify(frame_type, f) if "receipt" in f.headers: receipt_frame = Frame("RECEIPT", {"receipt-id": f.headers["receipt"]}) lines = convert_frame(receipt_frame) self.send(encode(pack(lines))) logging.debug("received frame: %r, headers=%r, body=%r", f.cmd, f.headers, f.body) def stop(self): self.running = False if hasattr(self.receiver_socket, "SHUT_RDWR"): self.receiver_socket.shutdown(socket.SHUT_RDWR) self.receiver_socket.close() self.disconnect_socket() Transport.stop(self) class MulticastConnection(BaseConnection, Protocol12): def __init__(self, encoding="utf-8"): self.transport = MulticastTransport(encoding) self.transport.set_listener("mcast-listener", self) self.transactions = {} Protocol12.__init__(self, self.transport, (0, 0)) def connect(self, username=None, passcode=None, wait=False, headers=None, **keyword_headers): """ :param str username: :param str passcode: :param bool wait: :param dict headers: :param keyword_headers: """ self.transport.start() def subscribe(self, destination, id, ack="auto", headers=None, **keyword_headers): """ :param str destination: :param str id: :param str ack: :param dict headers: :param keyword_headers: """ self.transport.subscriptions[id] = destination def unsubscribe(self, id, headers=None, **keyword_headers): """ :param str id: :param dict headers: :param keyword_headers: """ del self.transport.subscriptions[id] def disconnect(self, receipt=None, headers=None, **keyword_headers): """ :param str receipt: :param dict headers: :param keyword_headers: """ Protocol12.disconnect(self, receipt, headers, **keyword_headers) self.transport.stop() def send_frame(self, cmd, headers=None, body=""): """ :param str cmd: :param dict headers: :param body: """ if headers is None: headers = {} frame = Frame(cmd, headers, body) if cmd == CMD_BEGIN: trans = headers[HDR_TRANSACTION] if trans in self.transactions: self.notify("error", Frame(None, {}, "Transaction %s already started" % trans)) else: self.transactions[trans] = [] elif cmd == CMD_COMMIT: trans = headers[HDR_TRANSACTION] if trans not in self.transactions: self.notify("error", Frame(None, {}, "Transaction %s not started" % trans)) else: for f in self.transactions[trans]: self.transport.transmit(f) del self.transactions[trans] elif cmd == CMD_ABORT: trans = headers["transaction"] del self.transactions[trans] else: if "transaction" in headers: trans = headers["transaction"] if trans not in self.transactions: self.transport.notify("error", Frame(None, {}, "Transaction %s not started" % trans)) return else: self.transactions[trans].append(frame) else: self.transport.transmit(frame) stomp.py-8.1.0/stomp/adapter/ws.py000066400000000000000000000460031440306521300170770ustar00rootroot00000000000000import errno import math import random import sys import time from time import monotonic import websocket try: from socket import SOL_SOCKET, SO_KEEPALIVE, SOL_TCP, TCP_KEEPIDLE, TCP_KEEPINTVL, TCP_KEEPCNT LINUX_KEEPALIVE_AVAIL = True except ImportError: LINUX_KEEPALIVE_AVAIL = False try: import ssl from ssl import SSLError DEFAULT_SSL_VERSION = ssl.PROTOCOL_TLS_CLIENT except (ImportError, AttributeError): ssl = None class SSLError(object): pass DEFAULT_SSL_VERSION = None try: from socket import IPPROTO_TCP MAC_KEEPALIVE_AVAIL = True except ImportError: MAC_KEEPALIVE_AVAIL = False from stomp.transport import BaseTransport, DEFAULT_SSL_VERSION from stomp.utils import * from stomp.connect import BaseConnection, StompConnection12 from stomp.protocol import Protocol12 from stomp.exception import * from stomp import logging class WSTransport(BaseTransport): """ Represents a STOMP client websocket 'transport'. Effectively this is the communications mechanism without the definition of the protocol. :param list((str,int)) host_and_ports: a list of (host, port) tuples :param bool prefer_localhost: if True and the local host is mentioned in the (host, port) tuples, try to connect to this first :param bool try_loopback_connect: if True and the local host is found in the host tuples, try connecting to it using loopback interface (127.0.0.1) :param float reconnect_sleep_initial: initial delay in seconds to wait before reattempting to establish a connection if connection to any of the hosts fails. :param float reconnect_sleep_increase: factor by which the sleep delay is increased after each connection attempt. For example, 0.5 means to wait 50% longer than before the previous attempt, 1.0 means wait twice as long, and 0.0 means keep the delay constant. :param float reconnect_sleep_max: maximum delay between connection attempts, regardless of the reconnect_sleep_increase. :param float reconnect_sleep_jitter: random additional time to wait (as a percentage of the time determined using the previous parameters) between connection attempts in order to avoid stampeding. For example, a value of 0.1 means to wait an extra 0%-10% (randomly determined) of the delay calculated using the previous three parameters. :param int reconnect_attempts_max: maximum attempts to reconnect (Can also be used for infinite attempts : `-1`) :param timeout: the timeout value to use when connecting the stomp socket :param keepalive: some operating systems support sending the occasional heart beat packets to detect when a connection fails. This parameter can either be set set to a boolean to turn on the default keepalive options for your OS, or as a tuple of values, which also enables keepalive packets, but specifies options specific to your OS implementation. For linux, supply ("linux", ka_idle, ka_intvl, ka_cnt) For macos, supply ("mac", ka_intvl) :param str vhost: specify a virtual hostname to provide in the 'host' header of the connection :param int recv_bytes: the number of bytes to use when calling recv """ def __init__(self, host_and_ports=None, prefer_localhost=True, try_loopback_connect=True, reconnect_sleep_initial=0.1, reconnect_sleep_increase=0.5, reconnect_sleep_jitter=0.1, reconnect_sleep_max=60.0, reconnect_attempts_max=3, timeout=None, keepalive=None, vhost=None, auto_decode=True, encoding="utf-8", recv_bytes=1024, is_eol_fc=is_eol_default, bind_host_port=None, ws_path=None, header=None): BaseTransport.__init__(self, auto_decode, encoding, is_eol_fc) if host_and_ports is None: logging.debug("no hosts_and_ports specified, adding default localhost") host_and_ports = [("localhost", 61613)] sorted_host_and_ports = [] sorted_host_and_ports.extend(host_and_ports) # # If localhost is preferred, make sure all (host, port) tuples that refer to the local host come first in # the list # if prefer_localhost: sorted_host_and_ports.sort(key=is_localhost) # # If the user wishes to attempt connecting to local ports using the loopback interface, for each (host, port) # tuple referring to a local host, add an entry with the host name replaced by 127.0.0.1 if it doesn't # exist already # loopback_host_and_ports = [] if try_loopback_connect: for host_and_port in sorted_host_and_ports: if is_localhost(host_and_port) == 1: port = host_and_port[1] if not (("127.0.0.1", port) in sorted_host_and_ports or ( "localhost", port) in sorted_host_and_ports): loopback_host_and_ports.append(("127.0.0.1", port)) # # Assemble the final, possibly sorted list of (host, port) tuples # self.__host_and_ports = [] self.__host_and_ports.extend(loopback_host_and_ports) self.__host_and_ports.extend(sorted_host_and_ports) self.__bind_host_port = bind_host_port self.__reconnect_sleep_initial = reconnect_sleep_initial self.__reconnect_sleep_increase = reconnect_sleep_increase self.__reconnect_sleep_jitter = reconnect_sleep_jitter self.__reconnect_sleep_max = reconnect_sleep_max self.__reconnect_attempts_max = reconnect_attempts_max self.__timeout = timeout self.socket = None self.__socket_semaphore = threading.BoundedSemaphore(1) self.current_host_and_port = None self.vhost = vhost self.ws_path = ws_path self.header = header # setup SSL self.__ssl_params = {} self.__keepalive = keepalive self.__recv_bytes = recv_bytes def is_connected(self): """ Return true if the socket managed by this connection is connected :rtype: bool """ try: return self.socket is not None and self.socket.getstatus() == 101 and BaseTransport.is_connected(self) except socket.error: return False def disconnect_socket(self): """ Disconnect the underlying socket connection """ self.running = False if self.socket is not None: if self.__need_ssl(): # # Even though we don't want to use the socket, unwrap is the only API method which does a proper SSL # shutdown # try: self.socket = self.socket.unwrap() except Exception: # # unwrap seems flaky on Win with the back-ported ssl mod, so catch any exception and log it # _, e, _ = sys.exc_info() logging.warning(e) elif hasattr(socket, "SHUT_RDWR"): try: self.socket.shutdown(socket.SHUT_RDWR) except socket.error: _, e, _ = sys.exc_info() # ignore when socket already closed if get_errno(e) != errno.ENOTCONN: logging.warning("Unable to issue SHUT_RDWR on socket because of error '%s'", e) # # split this into a separate check, because sometimes the socket is nulled between shutdown and this call # if self.socket is not None: try: self.socket.close() except socket.error: _, e, _ = sys.exc_info() logging.warning("unable to close socket because of error '%s'", e) self.current_host_and_port = None self.socket = None if not self.notified_on_disconnect: self.notify("disconnected") def send(self, encoded_frame): """ :param bytes encoded_frame: """ if self.socket is not None: try: with self.__socket_semaphore: self.socket.send(encoded_frame) except Exception: _, e, _ = sys.exc_info() logging.error("error sending frame", exc_info=True) raise e else: raise NotConnectedException() def receive(self): """ :rtype: bytes """ try: return self.socket.recv().encode() except socket.error: _, e, _ = sys.exc_info() if get_errno(e) in (errno.EAGAIN, errno.EINTR): logging.debug("socket read interrupted, restarting") raise InterruptedException() if self.is_connected(): raise def cleanup(self): """ Close the socket and clear the current host and port details. """ try: self.socket.close() except: pass # ignore errors when attempting to close socket self.socket = None def __enable_keepalive(self): def try_setsockopt(sock, name, fam, opt, val): if val is None: return True # no value to set always works try: sock.setsockopt(fam, opt, val) logging.info("keepalive: set %r option to %r on socket", name, val) except: logging.error("keepalive: unable to set %r option to %r on socket", name, val) return False return True ka = self.__keepalive if not ka: return if ka is True: ka_sig = "auto" ka_args = () else: try: ka_sig = ka[0] ka_args = ka[1:] except Exception: logging.error("keepalive: bad specification %r", ka) return if ka_sig == "auto": if LINUX_KEEPALIVE_AVAIL: ka_sig = "linux" ka_args = None logging.info("keepalive: autodetected linux-style support") elif MAC_KEEPALIVE_AVAIL: ka_sig = "mac" ka_args = None logging.info("keepalive: autodetected mac-style support") else: logging.error("keepalive: unable to detect any implementation, DISABLED!") return if ka_sig == "linux": logging.info("keepalive: activating linux-style support") if ka_args is None: logging.info("keepalive: using system defaults") ka_args = (None, None, None) ka_idle, ka_intvl, ka_cnt = ka_args if try_setsockopt(self.socket, "enable", SOL_SOCKET, SO_KEEPALIVE, 1): try_setsockopt(self.socket, "idle time", SOL_TCP, TCP_KEEPIDLE, ka_idle) try_setsockopt(self.socket, "interval", SOL_TCP, TCP_KEEPINTVL, ka_intvl) try_setsockopt(self.socket, "count", SOL_TCP, TCP_KEEPCNT, ka_cnt) elif ka_sig == "mac": logging.info("keepalive: activating mac-style support") if ka_args is None: logging.info("keepalive: using system defaults") ka_args = (3,) ka_intvl = ka_args if try_setsockopt(self.socket, "enable", SOL_SOCKET, SO_KEEPALIVE, 1): try_setsockopt(self.socket, socket.IPPROTO_TCP, 0x10, ka_intvl) else: logging.error("keepalive: implementation %r not recognized or not supported", ka_sig) def attempt_connection(self): """ Try connecting to the (host, port) tuples specified at construction time. """ self.connection_error = False sleep_exp = 1 connect_count = 0 logging.info("attempt reconnection (%s, %s, %s)", self.running, self.socket, connect_count) while self.running and self.socket is None and (connect_count < self.__reconnect_attempts_max or self.__reconnect_attempts_max == -1): for host_and_port in self.__host_and_ports: try: logging.info("attempting connection to host %s, port %s", host_and_port[0], host_and_port[1]) #websocket.enableTrace(True) self.current_host_and_port = host_and_port path = "/" if self.ws_path: path = self.ws_path header = {} if self.header is not None: header = self.header if self.__need_ssl(): scheme = "wss" else: scheme = "ws" self.socket = websocket.create_connection( f"{scheme}://{host_and_port[0]}:{host_and_port[1]}{path}", header=self.header, sslopt=self.get_ssl() ) logging.info("established connection to host %s, port %s", host_and_port[0], host_and_port[1]) break except (OSError, AssertionError) as exc: self.socket = None connect_count += 1 logging.warning("Could not connect to host %s, port %s: %s", host_and_port[0], host_and_port[1], str(exc), exc_info=logging.verbose) if self.socket is None: sleep_duration = (min(self.__reconnect_sleep_max, ((self.__reconnect_sleep_initial / (1.0 + self.__reconnect_sleep_increase)) * math.pow(1.0 + self.__reconnect_sleep_increase, sleep_exp))) * (1.0 + random.random() * self.__reconnect_sleep_jitter)) sleep_end = monotonic() + sleep_duration logging.debug("sleeping for %.1f seconds before attempting reconnect", sleep_duration) while self.running and monotonic() < sleep_end: time.sleep(0.2) if sleep_duration < self.__reconnect_sleep_max: sleep_exp += 1 if not self.socket: raise ConnectFailedException() def set_ssl(self, for_hosts=[], key_file=None, cert_file=None, ca_certs=None, cert_validator=None, ssl_version=DEFAULT_SSL_VERSION, password=None): """ Sets up SSL configuration for the given hosts. This ensures socket is wrapped in a SSL connection, raising an exception if the SSL module can't be found. :param for_hosts: a list of tuples describing hosts this SSL configuration should be applied to :param cert_file: the path to a X509 certificate :param key_file: the path to a X509 key file :param ca_certs: the path to the a file containing CA certificates to validate the server against. If this is not set, server side certificate validation is not done. :param cert_validator: function which performs extra validation on the client certificate, for example checking the returned certificate has a commonName attribute equal to the hostname (to avoid man in the middle attacks). The signature is: (OK, err_msg) = validation_function(cert, hostname) where OK is a boolean, and cert is a certificate structure as returned by ssl.SSLSocket.getpeercert() :param ssl_version: SSL protocol to use for the connection. This should be one of the PROTOCOL_x constants provided by the ssl module. The default is ssl.PROTOCOL_TLSv1 :param password: SSL password """ if not ssl: raise Exception("SSL connection requested, but SSL library not found") for host_port in for_hosts: self.__ssl_params[host_port] = dict(key_file=key_file, cert_file=cert_file, ca_certs=ca_certs, cert_validator=cert_validator, ssl_version=ssl_version, password=password) def __need_ssl(self, host_and_port=None): """ Whether current host needs SSL or not. :param (str,int) host_and_port: the host/port pair to check, default current_host_and_port """ if not host_and_port: host_and_port = self.current_host_and_port return host_and_port in self.__ssl_params def get_ssl(self, host_and_port=None): """ Get SSL params for the given host. :param (str,int) host_and_port: the host/port pair we want SSL params for, default current_host_and_port """ if not host_and_port: host_and_port = self.current_host_and_port return self.__ssl_params.get(host_and_port) class WSStompConnection(StompConnection12): """ Represents a 1.2 connection (comprising transport plus 1.2 protocol class). See :py:class:`stomp.transport.Transport` for details on the initialisation parameters. """ def __init__(self, host_and_ports=None, prefer_localhost=True, try_loopback_connect=True, reconnect_sleep_initial=0.1, reconnect_sleep_increase=0.5, reconnect_sleep_jitter=0.1, reconnect_sleep_max=60.0, reconnect_attempts_max=3, timeout=None, heartbeats=(0, 0), keepalive=None, vhost=None, auto_decode=True, encoding="utf-8", auto_content_length=True, heart_beat_receive_scale=1.5, bind_host_port=None, ws=None, ws_path=None, header=None): transport = WSTransport(host_and_ports, prefer_localhost, try_loopback_connect, reconnect_sleep_initial, reconnect_sleep_increase, reconnect_sleep_jitter, reconnect_sleep_max, reconnect_attempts_max, timeout, keepalive, vhost, auto_decode, encoding, bind_host_port=bind_host_port, header=header, ws_path=ws_path) BaseConnection.__init__(self, transport) Protocol12.__init__(self, transport, heartbeats, auto_content_length, heart_beat_receive_scale=heart_beat_receive_scale) stomp.py-8.1.0/stomp/colours.py000066400000000000000000000004251440306521300165120ustar00rootroot00000000000000"""Colour 'constants' used by the command line client. """ import platform if platform.system().lower() != "windows": GREEN = "\33[32m" RED = "\33[31m" NO_COLOUR = "\33[0m" BOLD = "\33[1m" else: GREEN = "" NO_COLOUR = "" RED = "" BOLD = "" stomp.py-8.1.0/stomp/connect.py000066400000000000000000000205771440306521300164670ustar00rootroot00000000000000"""Main entry point for clients to create a STOMP connection. Provides connection classes for `1.0 `_, `1.1 `_, and `1.2 `_ versions of the STOMP protocol. """ from stomp.protocol import * from stomp.transport import * from stomp.utils import get_uuid class BaseConnection(Publisher): """ Base class for all connection classes. """ def __init__(self, transport): """ :param Transport transport: """ self.transport = transport def __enter__(self): self.disconnect_receipt_id = get_uuid() self.disconnect_listener = WaitingListener(self.disconnect_receipt_id) self.set_listener("ZZZZZ-disconnect-listener", self.disconnect_listener) return self def disconnect(self, receipt=None, headers=None, **keyword_headers): pass def __exit__(self, exc_type, exc_val, exc_tb): self.disconnect(self.disconnect_receipt_id) self.disconnect_listener.wait_on_receipt() self.disconnect_listener.wait_on_disconnected() def set_listener(self, name, listener): """ :param str name: :param ConnectionListener listener: """ self.transport.set_listener(name, listener) def remove_listener(self, name): """ :param str name: """ self.transport.remove_listener(name) def get_listener(self, name): """ :param str name: :rtype: ConnectionListener """ return self.transport.get_listener(name) def is_connected(self): """ :rtype: bool """ return self.transport.is_connected() def set_receipt(self, receipt_id, value): self.transport.set_receipt(receipt_id, value) def set_ssl(self, *args, **kwargs): self.transport.set_ssl(*args, **kwargs) def get_ssl(self, host_and_port=None): return self.transport.get_ssl(host_and_port) class StompConnection10(BaseConnection, Protocol10): """ Represents a 1.0 connection (comprising transport plus 1.0 protocol class). See :py:class:`stomp.transport.Transport` for details on the initialisation parameters. """ def __init__(self, host_and_ports=None, prefer_localhost=True, try_loopback_connect=True, reconnect_sleep_initial=0.1, reconnect_sleep_increase=0.5, reconnect_sleep_jitter=0.1, reconnect_sleep_max=60.0, reconnect_attempts_max=3, timeout=None, keepalive=None, auto_decode=True, encoding="utf-8", auto_content_length=True, bind_host_port=None): transport = Transport(host_and_ports, prefer_localhost, try_loopback_connect, reconnect_sleep_initial, reconnect_sleep_increase, reconnect_sleep_jitter, reconnect_sleep_max, reconnect_attempts_max, timeout, keepalive, None, auto_decode, encoding, bind_host_port=bind_host_port) BaseConnection.__init__(self, transport) Protocol10.__init__(self, transport, auto_content_length) def connect(self, *args, **kwargs): self.transport.start() Protocol10.connect(self, *args, **kwargs) def disconnect(self, receipt=None, headers=None, **keyword_headers): """ Call the protocol disconnection, and then stop the transport itself. :param str receipt: the receipt to use with the disconnect :param dict headers: a map of any additional headers to send with the disconnection :param keyword_headers: any additional headers to send with the disconnection """ Protocol10.disconnect(self, receipt, headers, **keyword_headers) if receipt is not None: self.transport.stop() class StompConnection11(BaseConnection, Protocol11): """ Represents a 1.1 connection (comprising transport plus 1.1 protocol class) See :py:class:`stomp.transport.Transport` for details on the initialisation parameters. """ def __init__(self, host_and_ports=None, prefer_localhost=True, try_loopback_connect=True, reconnect_sleep_initial=0.1, reconnect_sleep_increase=0.5, reconnect_sleep_jitter=0.1, reconnect_sleep_max=60.0, reconnect_attempts_max=3, timeout=None, heartbeats=(0, 0), keepalive=None, vhost=None, auto_decode=True, encoding="utf-8", auto_content_length=True, heart_beat_receive_scale=1.5, bind_host_port=None): transport = Transport(host_and_ports, prefer_localhost, try_loopback_connect, reconnect_sleep_initial, reconnect_sleep_increase, reconnect_sleep_jitter, reconnect_sleep_max, reconnect_attempts_max, timeout, keepalive, vhost, auto_decode, encoding, bind_host_port=bind_host_port) BaseConnection.__init__(self, transport) Protocol11.__init__(self, transport, heartbeats, auto_content_length, heart_beat_receive_scale=heart_beat_receive_scale) def connect(self, *args, **kwargs): self.transport.start() Protocol11.connect(self, *args, **kwargs) def disconnect(self, receipt=None, headers=None, **keyword_headers): """ Call the protocol disconnection, and then stop the transport itself. :param str receipt: the receipt to use with the disconnect :param dict headers: a map of any additional headers to send with the disconnection :param keyword_headers: any additional headers to send with the disconnection """ Protocol11.disconnect(self, receipt, headers, **keyword_headers) if receipt is not None: self.transport.stop() class StompConnection12(BaseConnection, Protocol12): """ Represents a 1.2 connection (comprising transport plus 1.2 protocol class). See :py:class:`stomp.transport.Transport` for details on the initialisation parameters. """ def __init__(self, host_and_ports=None, prefer_localhost=True, try_loopback_connect=True, reconnect_sleep_initial=0.1, reconnect_sleep_increase=0.5, reconnect_sleep_jitter=0.1, reconnect_sleep_max=60.0, reconnect_attempts_max=3, timeout=None, heartbeats=(0, 0), keepalive=None, vhost=None, auto_decode=True, encoding="utf-8", auto_content_length=True, heart_beat_receive_scale=1.5, bind_host_port=None): transport = Transport(host_and_ports, prefer_localhost, try_loopback_connect, reconnect_sleep_initial, reconnect_sleep_increase, reconnect_sleep_jitter, reconnect_sleep_max, reconnect_attempts_max, timeout, keepalive, vhost, auto_decode, encoding, bind_host_port=bind_host_port) BaseConnection.__init__(self, transport) Protocol12.__init__(self, transport, heartbeats, auto_content_length, heart_beat_receive_scale=heart_beat_receive_scale) def connect(self, *args, **kwargs): self.transport.start() Protocol12.connect(self, *args, **kwargs) def disconnect(self, receipt=None, headers=None, **keyword_headers): """ Call the protocol disconnection, and then stop the transport itself. :param str receipt: the receipt to use with the disconnect :param dict headers: a map of any additional headers to send with the disconnection :param keyword_headers: any additional headers to send with the disconnection """ Protocol12.disconnect(self, receipt, headers, **keyword_headers) if receipt is not None: self.transport.stop() @staticmethod def is_eol(c): return c == b"\x0a" or c == b"\x0d\x0a" stomp.py-8.1.0/stomp/constants.py000066400000000000000000000012551440306521300170420ustar00rootroot00000000000000"""The STOMP command and header name strings. """ HDR_ACCEPT_VERSION = "accept-version" HDR_ACK = "ack" HDR_CONTENT_LENGTH = "content-length" HDR_CONTENT_TYPE = "content-type" HDR_DESTINATION = "destination" HDR_HEARTBEAT = "heart-beat" HDR_HOST = "host" HDR_ID = "id" HDR_MESSAGE_ID = "message-id" HDR_LOGIN = "login" HDR_PASSCODE = "passcode" HDR_RECEIPT = "receipt" HDR_SUBSCRIPTION = "subscription" HDR_TRANSACTION = "transaction" CMD_ABORT = "ABORT" CMD_ACK = "ACK" CMD_BEGIN = "BEGIN" CMD_COMMIT = "COMMIT" CMD_CONNECT = "CONNECT" CMD_DISCONNECT = "DISCONNECT" CMD_NACK = "NACK" CMD_STOMP = "STOMP" CMD_SEND = "SEND" CMD_SUBSCRIBE = "SUBSCRIBE" CMD_UNSUBSCRIBE = "UNSUBSCRIBE" stomp.py-8.1.0/stomp/exception.py000066400000000000000000000015331440306521300170230ustar00rootroot00000000000000"""Errors thrown by stomp.py connections. """ class StompException(Exception): """ Common exception class. All specific stomp.py exceptions are subclasses of StompException, allowing the library user to catch all current and future library exceptions. """ class ConnectionClosedException(StompException): """ Raised in the receiver thread when the connection has been closed by the server. """ class NotConnectedException(StompException): """ Raised when there is currently no server connection. """ class ConnectFailedException(StompException): """ Raised by Connection.attempt_connection when reconnection attempts have exceeded Connection.__reconnect_attempts_max. """ class InterruptedException(StompException): """ Raised by receive when data read is interrupted. """ stomp.py-8.1.0/stomp/listener.py000066400000000000000000000473651440306521300166670ustar00rootroot00000000000000"""Various listeners for using with stomp.py connections. """ import sys import threading import time from time import monotonic import stomp.exception as exception import stomp.utils as utils from stomp.constants import * from stomp import logging class Publisher(object): """ Simply a registry of listeners. """ def set_listener(self, name, listener): """ Set a named listener to use with this connection. See :py:class:`stomp.listener.ConnectionListener` :param str name: the name of the listener :param ConnectionListener listener: the listener object """ pass def remove_listener(self, name): """ Remove a listener. :param str name: the name of the listener to remove """ pass def get_listener(self, name): """ Return the named listener. :param str name: the listener to return :rtype: ConnectionListener """ return None class ConnectionListener(object): """ This class should be used as a base class for objects registered using Connection.set_listener(). """ def on_connecting(self, host_and_port): """ Called by the STOMP connection once a TCP/IP connection to the STOMP server has been established or re-established. Note that at this point, no connection has been established on the STOMP protocol level. For this, you need to invoke the "connect" method on the connection. :param (str,int) host_and_port: a tuple containing the host name and port number to which the connection has been established. """ pass def on_connected(self, frame): """ Called by the STOMP connection when a CONNECTED frame is received (after a connection has been established or re-established). :param Frame frame: the stomp frame """ pass def on_disconnecting(self): """ Called before a DISCONNECT frame is sent. """ pass def on_disconnected(self): """ Called by the STOMP connection when a TCP/IP connection to the STOMP server has been lost. No messages should be sent via the connection until it has been reestablished. """ pass def on_heartbeat_timeout(self): """ Called by the STOMP connection when a heartbeat message has not been received beyond the specified period. """ pass def on_before_message(self, frame): """ Called by the STOMP connection before a message is returned to the client app. Returns a tuple containing the headers and body (so that implementing listeners can pre-process the content). :param Frame frame: the stomp frame """ pass def on_message(self, frame): """ Called by the STOMP connection when a MESSAGE frame is received. :param Frame frame: the stomp frame """ pass def on_receipt(self, frame): """ Called by the STOMP connection when a RECEIPT frame is received, sent by the server if requested by the client using the 'receipt' header. :param Frame frame: the stomp frame """ pass def on_error(self, frame): """ Called by the STOMP connection when an ERROR frame is received. :param Frame frame: the stomp frame """ pass def on_send(self, frame): """ Called by the STOMP connection when it is in the process of sending a message :param Frame frame: the stomp frame """ pass def on_heartbeat(self): """ Called on receipt of a heartbeat. """ pass def on_receiver_loop_completed(self, frame): """ Called when the connection receiver_loop has finished. """ pass class HeartbeatListener(ConnectionListener): """ Listener used to handle STOMP heartbeating. """ def __init__(self, transport, heartbeats, heart_beat_receive_scale=1.5): self.running = False self.transport = transport self.heartbeats = heartbeats self.received_heartbeat = None self.heartbeat_thread = None self.next_outbound_heartbeat = None self.heart_beat_receive_scale = heart_beat_receive_scale self.heartbeat_terminate_event = threading.Event() self.disconnecting = False def on_connected(self, frame): """ Once the connection is established, and 'heart-beat' is found in the headers, we calculate the real heartbeat numbers (based on what the server sent and what was specified by the client) - if the heartbeats are not 0, we start up the heartbeat loop accordingly. :param Frame frame: the stomp frame """ self.disconnecting = False if "heart-beat" in frame.headers: self.heartbeats = utils.calculate_heartbeats( frame.headers["heart-beat"].replace(" ", "").split(","), self.heartbeats) logging.debug("heartbeats calculated %s", str(self.heartbeats)) if self.heartbeats != (0, 0): self.send_sleep = self.heartbeats[0] / 1000 # by default, receive gets an additional grace of 50% # set a different heart-beat-receive-scale when creating the connection to override that self.receive_sleep = (self.heartbeats[1] / 1000) * self.heart_beat_receive_scale logging.debug("set receive_sleep to %s, send_sleep to %s", self.receive_sleep, self.send_sleep) # Give grace of receiving the first heartbeat self.received_heartbeat = monotonic() + self.receive_sleep self.running = True if self.heartbeat_thread is None: self.heartbeat_thread = utils.default_create_thread( self.__heartbeat_loop) self.heartbeat_thread.name = "StompHeartbeat%s" % \ getattr(self.heartbeat_thread, "name", "Thread") def on_disconnected(self): self.running = False self.heartbeat_terminate_event.set() def on_disconnecting(self): self.disconnecting = True def on_message(self, frame): """ Reset the last received time whenever a message is received. :param Frame frame: the stomp frame """ # reset the heartbeat for any received message self.__update_heartbeat() def on_receipt(self, *_): """ Reset the last received time whenever a receipt is received. """ self.__update_heartbeat() def on_error(self, *_): """ Reset the last received time whenever an error is received. """ self.__update_heartbeat() def on_heartbeat(self): """ Reset the last received time whenever a heartbeat message is received. """ self.__update_heartbeat() def on_send(self, frame): """ Add the heartbeat header to the frame when connecting, and bump next outbound heartbeat timestamp. :param Frame frame: the Frame object """ if frame.cmd in [CMD_CONNECT, CMD_STOMP] and self.heartbeats != (0, 0): frame.headers[HDR_HEARTBEAT] = "%s,%s" % self.heartbeats if self.next_outbound_heartbeat is not None: self.next_outbound_heartbeat = monotonic() + self.send_sleep def __update_heartbeat(self): # Honour any grace that has been already included if self.received_heartbeat is None: return now = monotonic() if now > self.received_heartbeat: self.received_heartbeat = now def __heartbeat_loop(self): """ Main loop for sending (and monitoring received) heartbeats. """ logging.debug("starting heartbeat loop") now = monotonic() # Setup the initial due time for the outbound heartbeat if self.send_sleep != 0: self.next_outbound_heartbeat = now + self.send_sleep logging.debug("calculated next outbound heartbeat as %s", self.next_outbound_heartbeat) while self.running: now = monotonic() next_events = [] if self.next_outbound_heartbeat is not None: next_events.append(self.next_outbound_heartbeat - now) if self.receive_sleep != 0: t = self.received_heartbeat + self.receive_sleep - now if t > 0: next_events.append(t) sleep_time = min(next_events) if next_events else 0 if sleep_time > 0: terminate = self.heartbeat_terminate_event.wait(sleep_time) if terminate: break now = monotonic() if not self.transport.is_connected() or self.disconnecting: time.sleep(self.send_sleep) continue if self.send_sleep != 0 and now > self.next_outbound_heartbeat: logging.debug("sending a heartbeat message at %s", now) try: self.transport.transmit(utils.Frame(None, {}, None)) except exception.NotConnectedException: logging.debug("lost connection, unable to send heartbeat") except Exception: _, e, _ = sys.exc_info() logging.debug("unable to send heartbeat, due to: %s", e) if self.receive_sleep != 0: diff_receive = now - self.received_heartbeat if diff_receive > self.receive_sleep: # heartbeat timeout logging.warning("heartbeat timeout: diff_receive=%s, time=%s, lastrec=%s", diff_receive, now, self.received_heartbeat) self.transport.set_connected(False) self.transport.disconnect_socket() self.transport.stop() for listener in self.transport.listeners.values(): listener.on_heartbeat_timeout() self.heartbeat_thread = None self.heartbeat_terminate_event.clear() if self.heartbeats != (0, 0): # don't bother logging this if heartbeats weren't setup to start with logging.debug("heartbeat loop ended") class WaitingListener(ConnectionListener): """ A listener which waits for a specific receipt to arrive. """ def __init__(self, receipt): """ :param str receipt: """ self.receipt_condition = threading.Condition() self.disconnect_condition = threading.Condition() self.receipt = receipt self.received = False self.disconnected = False def on_receipt(self, frame): """ If the receipt id can be found in the headers, then notify the waiting thread. :param Frame frame: the stomp frame """ if "receipt-id" in frame.headers and frame.headers["receipt-id"] == self.receipt: with self.receipt_condition: self.received = True self.receipt_condition.notify() def on_disconnected(self): with self.disconnect_condition: self.disconnected = True self.disconnect_condition.notify() def wait_on_receipt(self): """ Wait until we receive a message receipt. """ with self.receipt_condition: while not self.received: self.receipt_condition.wait() self.received = False def wait_on_disconnected(self): """ Wait until disconnected. """ with self.disconnect_condition: while not self.disconnected: self.disconnect_condition.wait() class StatsListener(ConnectionListener): """ A connection listener for recording statistics on messages sent and received. """ def __init__(self): # The number of errors received self.errors = 0 # The number of connections established self.connections = 0 # The number of disconnections self.disconnects = 0 # The number of messages received self.messages = 0 # The number of messages sent self.messages_sent = 0 # The number of heartbeat timeouts self.heartbeat_timeouts = 0 # The number of heartbeats self.heartbeat_count = 0 def on_disconnected(self): """ Increment the disconnect count. See :py:meth:`ConnectionListener.on_disconnected` """ self.disconnects += 1 logging.info("disconnected (x %s)", self.disconnects) def on_error(self, frame): """ Increment the error count. See :py:meth:`ConnectionListener.on_error` :param Frame frame: the stomp frame """ if logging.isEnabledFor(logging.DEBUG): logging.debug("received an error %s [%s]", frame.body, frame.headers) else: logging.info("received an error %s", frame.body) self.errors += 1 def on_connecting(self, host_and_port): """ Increment the connection count. See :py:meth:`ConnectionListener.on_connecting` :param (str,int) host_and_port: the host and port as a tuple """ logging.info("connecting %s %s (x %s)", host_and_port[0], host_and_port[1], self.connections) self.connections += 1 def on_message(self, frame): """ Increment the message received count. See :py:meth:`ConnectionListener.on_message` :param Frame frame: the stomp frame """ self.messages += 1 def on_send(self, frame): """ Increment the send count. See :py:meth:`ConnectionListener.on_send` :param Frame frame: """ self.messages_sent += 1 def on_heartbeat_timeout(self): """ Increment the heartbeat timeout. See :py:meth:`ConnectionListener.on_heartbeat_timeout` """ logging.debug("received heartbeat timeout") self.heartbeat_timeouts += 1 def on_heartbeat(self): """ Increment the heartbeat count. See :py:meth:`ConnectionListener.on_heartbeat` """ self.heartbeat_count += 1 def __str__(self): """ Return a string containing the current statistics (messages sent and received, errors, etc) """ return '''Connections: %s Disconnects: %s Messages sent: %s Messages received: %s Heartbeats received: %s Errors: %s''' % (self.connections, self.disconnects, self.messages_sent, self.messages, self.heartbeat_count, self.errors) class PrintingListener(ConnectionListener): def __init__(self, print_to_log=False): self.print_to_log = print_to_log def __print(self, msg, *args): if self.print_to_log: logging.info(msg, *args) else: print(msg % args) def on_connecting(self, host_and_port): """ :param (str,int) host_and_port: """ self.__print("on_connecting %s %s", *host_and_port) def on_connected(self, frame): """ :param Frame frame: the stomp frame """ self.__print("on_connected %s %s", frame.headers, frame.body) def on_disconnected(self): self.__print("on_disconnected") def on_heartbeat_timeout(self): self.__print("on_heartbeat_timeout") def on_before_message(self, frame): """ :param Frame frame: the stomp frame """ self.__print("on_before_message %s %s", frame.headers, frame.body) def on_message(self, frame): """ :param Frame frame: the stomp frame """ self.__print("on_message %s %s", frame.headers, frame.body) def on_receipt(self, frame): """ :param Frame frame: the stomp frame """ self.__print("on_receipt %s %s", frame.headers, frame.body) def on_error(self, frame): """ :param Frame frame: the stomp frame """ self.__print("on_error %s %s", frame.headers, frame.body) def on_send(self, frame): """ :param Frame frame: the stomp frame """ self.__print("on_send %s %s %s", frame.cmd, utils.clean_headers(frame.headers), frame.body) def on_heartbeat(self): self.__print("on_heartbeat") class TestListener(StatsListener, WaitingListener, PrintingListener): """ Implementation of StatsListener and WaitingListener. Useful for testing. """ def __init__(self, receipt=None, print_to_log=False): """ :param str receipt: """ StatsListener.__init__(self) PrintingListener.__init__(self, print_to_log) WaitingListener.__init__(self, receipt) self.message_list = [] self.message_condition = threading.Condition() self.message_received = False self.heartbeat_condition = threading.Condition() self.heartbeat_received = False self.timestamp = time.strftime("%Y%m%d%H%M%S") def wait_for_message(self): with self.message_condition: while not self.message_received: self.message_condition.wait() self.message_received = False def get_latest_message(self): return self.message_list[-1] def wait_for_heartbeat(self): with self.heartbeat_condition: while not self.heartbeat_received: self.heartbeat_condition.wait() self.heartbeat_received = False def on_connecting(self, host_and_port): StatsListener.on_connecting(self, host_and_port) PrintingListener.on_connecting(self, host_and_port) WaitingListener.on_connecting(self, host_and_port) def on_connected(self, frame): StatsListener.on_connected(self, frame) PrintingListener.on_connected(self, frame) WaitingListener.on_connected(self, frame) def on_disconnected(self): StatsListener.on_disconnected(self) PrintingListener.on_disconnected(self) WaitingListener.on_disconnected(self) def on_heartbeat_timeout(self): StatsListener.on_heartbeat_timeout(self) PrintingListener.on_heartbeat_timeout(self) WaitingListener.on_heartbeat_timeout(self) def on_before_message(self, frame): StatsListener.on_before_message(self, frame) PrintingListener.on_before_message(self, frame) WaitingListener.on_before_message(self, frame) def on_message(self, frame): """ :param Frame frame: the stomp frame """ StatsListener.on_message(self, frame) PrintingListener.on_message(self, frame) self.message_list.append((frame.headers, frame.body)) with self.message_condition: self.message_received = True self.message_condition.notify() def on_receipt(self, frame): StatsListener.on_receipt(self, frame) PrintingListener.on_receipt(self, frame) WaitingListener.on_receipt(self, frame) def on_error(self, frame): StatsListener.on_error(self, frame) PrintingListener.on_error(self, frame) WaitingListener.on_error(self, frame) def on_send(self, frame): StatsListener.on_send(self, frame) PrintingListener.on_send(self, frame) WaitingListener.on_send(self, frame) def on_heartbeat(self): StatsListener.on_heartbeat(self) PrintingListener.on_heartbeat(self) with self.heartbeat_condition: self.heartbeat_received = True self.heartbeat_condition.notify() def on_receiver_loop_completed(self, frame): StatsListener.on_receiver_loop_completed(self, frame) PrintingListener.on_receiver_loop_completed(self, frame) WaitingListener.on_receiver_loop_completed(self, frame) stomp.py-8.1.0/stomp/logging.py000066400000000000000000000012271440306521300164530ustar00rootroot00000000000000import logging DEBUG = logging.DEBUG INFO = logging.INFO verbose = False __logger = logging.getLogger("stomp.py") debug = __logger.debug info = __logger.info warning = __logger.warning error = __logger.error isEnabledFor = __logger.isEnabledFor setLevel = __logger.setLevel def log_to_stdout(verbose_logging=True): import sys handler = logging.StreamHandler(sys.stdout) handler.setLevel(logging.DEBUG) formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') handler.setFormatter(formatter) __logger.addHandler(handler) __logger.setLevel(logging.DEBUG) global verbose verbose = verbose_loggingstomp.py-8.1.0/stomp/protocol.py000066400000000000000000000543471440306521300167010ustar00rootroot00000000000000"""Provides the 1.0, 1.1 and 1.2 protocol classes. """ from stomp.exception import ConnectFailedException from stomp.listener import * class Protocol10(ConnectionListener): """ Represents version 1.0 of the protocol (see https://stomp.github.io/stomp-specification-1.0.html). Most users should not instantiate the protocol directly. See :py:mod:`stomp.connect` for connection classes. :param transport: :param bool auto_content_length: Whether to calculate and send the content-length header automatically if it has not been set """ def __init__(self, transport, auto_content_length=True): self.transport = transport self.auto_content_length = auto_content_length transport.set_listener("protocol-listener", self) self.version = "1.0" def send_frame(self, cmd, headers=None, body=""): """ Encode and send a stomp frame through the underlying transport. :param str cmd: the protocol command :param dict headers: a map of headers to include in the frame :param body: the content of the message """ frame = utils.Frame(cmd, headers, body) self.transport.transmit(frame) def abort(self, transaction, headers=None, **keyword_headers): """ Abort a transaction. :param str transaction: the identifier of the transaction :param dict headers: a map of any additional headers the broker requires :param keyword_headers: any additional headers the broker requires """ assert transaction is not None, "'transaction' is required" headers = utils.merge_headers([headers, keyword_headers]) headers[HDR_TRANSACTION] = transaction self.send_frame(CMD_ABORT, headers) def ack(self, id, transaction=None, receipt=None): """ Acknowledge 'consumption' of a message by id. :param str id: identifier of the message :param str transaction: include the acknowledgement in the specified transaction :param str receipt: the receipt id """ assert id is not None, "'id' is required" headers = {HDR_MESSAGE_ID: id} if transaction: headers[HDR_TRANSACTION] = transaction if receipt: headers[HDR_RECEIPT] = receipt self.send_frame(CMD_ACK, headers) def begin(self, transaction=None, headers=None, **keyword_headers): """ Begin a transaction. :param str transaction: the identifier for the transaction (optional - if not specified a unique transaction id will be generated) :param dict headers: a map of any additional headers the broker requires :param keyword_headers: any additional headers the broker requires :return: the transaction id :rtype: str """ headers = utils.merge_headers([headers, keyword_headers]) if not transaction: transaction = utils.get_uuid() headers[HDR_TRANSACTION] = transaction self.send_frame(CMD_BEGIN, headers) return transaction def commit(self, transaction=None, headers=None, **keyword_headers): """ Commit a transaction. :param str transaction: the identifier for the transaction :param dict headers: a map of any additional headers the broker requires :param keyword_headers: any additional headers the broker requires """ assert transaction is not None, "'transaction' is required" headers = utils.merge_headers([headers, keyword_headers]) headers[HDR_TRANSACTION] = transaction self.send_frame(CMD_COMMIT, headers) def connect(self, username=None, passcode=None, wait=False, headers=None, **keyword_headers): """ Start a connection. :param str username: the username to connect with :param str passcode: the password used to authenticate with :param bool wait: if True, wait for the connection to be established/acknowledged :param dict headers: a map of any additional headers the broker requires :param keyword_headers: any additional headers the broker requires """ cmd = CMD_CONNECT headers = utils.merge_headers([headers, keyword_headers]) headers[HDR_ACCEPT_VERSION] = self.version if username is not None: headers[HDR_LOGIN] = username if passcode is not None: headers[HDR_PASSCODE] = passcode self.send_frame(cmd, headers) if wait: self.transport.wait_for_connection() if self.transport.connection_error: raise ConnectFailedException() def disconnect(self, receipt=None, headers=None, **keyword_headers): """ Disconnect from the server. :param str receipt: the receipt to use (once the server acknowledges that receipt, we're officially disconnected; optional - if not specified a unique receipt id will be generated) :param dict headers: a map of any additional headers the broker requires :param keyword_headers: any additional headers the broker requires """ if not self.transport.is_connected(): logging.debug("not sending disconnect, already disconnected") return headers = utils.merge_headers([headers, keyword_headers]) rec = receipt or utils.get_uuid() headers[HDR_RECEIPT] = rec self.set_receipt(rec, CMD_DISCONNECT) self.send_frame(CMD_DISCONNECT, headers) def send(self, destination, body, content_type=None, headers=None, **keyword_headers): """ Send a message to a destination. :param str destination: the destination of the message (e.g. queue or topic name) :param body: the content of the message :param str content_type: the content type of the message :param dict headers: a map of any additional headers the broker requires :param keyword_headers: any additional headers the broker requires """ assert destination is not None, "'destination' is required" assert body is not None, "'body' is required" headers = utils.merge_headers([headers, keyword_headers]) headers[HDR_DESTINATION] = destination if content_type: headers[HDR_CONTENT_TYPE] = content_type if self.auto_content_length and body and HDR_CONTENT_LENGTH not in headers: headers[HDR_CONTENT_LENGTH] = len(body) self.send_frame(CMD_SEND, headers, body) def subscribe(self, destination, id=None, ack="auto", headers=None, **keyword_headers): """ Subscribe to a destination. :param str destination: the topic or queue to subscribe to :param str id: a unique id to represent the subscription :param str ack: acknowledgement mode, either auto, client, or client-individual (see http://stomp.github.io/stomp-specification-1.2.html#SUBSCRIBE_ack_Header) for more information :param dict headers: a map of any additional headers the broker requires :param keyword_headers: any additional headers the broker requires """ assert destination is not None, "'destination' is required" headers = utils.merge_headers([headers, keyword_headers]) headers[HDR_DESTINATION] = destination if id: headers[HDR_ID] = id headers[HDR_ACK] = ack self.send_frame(CMD_SUBSCRIBE, headers) def unsubscribe(self, destination=None, id=None, headers=None, **keyword_headers): """ Unsubscribe from a destination by either id or the destination name. :param str destination: the name of the topic or queue to unsubscribe from :param str id: the unique identifier of the topic or queue to unsubscribe from :param dict headers: a map of any additional headers the broker requires :param keyword_headers: any additional headers the broker requires """ assert id is not None or destination is not None, "'id' or 'destination' is required" headers = utils.merge_headers([headers, keyword_headers]) if id: headers[HDR_ID] = id if destination: headers[HDR_DESTINATION] = destination self.send_frame(CMD_UNSUBSCRIBE, headers) class Protocol11(HeartbeatListener, ConnectionListener): """ Represents version 1.1 of the protocol (see https://stomp.github.io/stomp-specification-1.1.html). Most users should not instantiate the protocol directly. See :py:mod:`stomp.connect` for connection classes. :param transport: :param (int,int) heartbeats: :param bool auto_content_length: Whether to calculate and send the content-length header automatically if it has not been set :param float heart_beat_receive_scale: how long to wait for a heartbeat before timing out, as a scale factor of receive time """ def __init__(self, transport, heartbeats=(0, 0), auto_content_length=True, heart_beat_receive_scale=1.5): HeartbeatListener.__init__(self, transport, heartbeats, heart_beat_receive_scale) self.transport = transport self.auto_content_length = auto_content_length transport.set_listener("protocol-listener", self) self.version = "1.1" def _escape_headers(self, headers): """ :param dict(str,str) headers: """ for key, val in headers.items(): try: val = val.replace("\\", "\\\\").replace("\n", "\\n").replace(":", "\\c") except: pass headers[key] = val def send_frame(self, cmd, headers=None, body=""): """ Encode and send a stomp frame through the underlying transport: :param str cmd: the protocol command :param dict headers: a map of headers to include in the frame :param body: the content of the message """ if cmd not in [CMD_CONNECT, CMD_STOMP]: if headers is None: headers = {} self._escape_headers(headers) frame = utils.Frame(cmd, headers, body) self.transport.transmit(frame) def abort(self, transaction, headers=None, **keyword_headers): """ Abort a transaction. :param str transaction: the identifier of the transaction :param dict headers: a map of any additional headers the broker requires :param keyword_headers: any additional headers the broker requires """ assert transaction is not None, "'transaction' is required" headers = utils.merge_headers([headers, keyword_headers]) headers[HDR_TRANSACTION] = transaction self.send_frame(CMD_ABORT, headers) def ack(self, id, subscription, transaction=None, receipt=None): """ Acknowledge 'consumption' of a message by id. :param str id: identifier of the message :param str subscription: the subscription this message is associated with :param str transaction: include the acknowledgement in the specified transaction :param str receipt: the receipt id """ assert id is not None, "'id' is required" assert subscription is not None, "'subscription' is required" headers = {HDR_MESSAGE_ID: id, HDR_SUBSCRIPTION: subscription} if transaction: headers[HDR_TRANSACTION] = transaction if receipt: headers[HDR_RECEIPT] = receipt self.send_frame(CMD_ACK, headers) def begin(self, transaction=None, headers=None, **keyword_headers): """ Begin a transaction. :param str transaction: the identifier for the transaction (optional - if not specified a unique transaction id will be generated) :param dict headers: a map of any additional headers the broker requires :param keyword_headers: any additional headers the broker requires :return: the transaction id :rtype: str """ headers = utils.merge_headers([headers, keyword_headers]) if not transaction: transaction = utils.get_uuid() headers[HDR_TRANSACTION] = transaction self.send_frame(CMD_BEGIN, headers) return transaction def commit(self, transaction=None, headers=None, **keyword_headers): """ Commit a transaction. :param str transaction: the identifier for the transaction :param dict headers: a map of any additional headers the broker requires :param keyword_headers: any additional headers the broker requires """ assert transaction is not None, "'transaction' is required" headers = utils.merge_headers([headers, keyword_headers]) headers[HDR_TRANSACTION] = transaction self.send_frame(CMD_COMMIT, headers) def connect(self, username=None, passcode=None, wait=False, headers=None, with_connect_command=False, **keyword_headers): """ Start a connection. :param str username: the username to connect with :param str passcode: the password used to authenticate with :param bool wait: if True, wait for the connection to be established/acknowledged :param dict headers: a map of any additional headers the broker requires :param with_connect_command: if True, use CONNECT command instead of STOMP :param keyword_headers: any additional headers the broker requires """ cmd = CMD_CONNECT if with_connect_command else CMD_STOMP headers = utils.merge_headers([headers, keyword_headers]) headers[HDR_ACCEPT_VERSION] = self.version if self.transport.vhost: headers[HDR_HOST] = self.transport.vhost if username is not None: headers[HDR_LOGIN] = username if passcode is not None: headers[HDR_PASSCODE] = passcode self.send_frame(cmd, headers) if wait: self.transport.wait_for_connection() if self.transport.connection_error: raise ConnectFailedException() def disconnect(self, receipt=None, headers=None, **keyword_headers): """ Disconnect from the server. :param str receipt: the receipt to use (once the server acknowledges that receipt, we're officially disconnected; optional - if not specified a unique receipt id will be generated) :param dict headers: a map of any additional headers the broker requires :param keyword_headers: any additional headers the broker requires """ if not self.transport.is_connected(): logging.debug("not sending disconnect, already disconnected") return headers = utils.merge_headers([headers, keyword_headers]) rec = receipt or utils.get_uuid() headers[HDR_RECEIPT] = rec self.set_receipt(rec, CMD_DISCONNECT) self.send_frame(CMD_DISCONNECT, headers) def nack(self, id, subscription, transaction=None, receipt=None, **keyword_headers): """ Let the server know that a message was not consumed. :param str id: the unique id of the message to nack :param str subscription: the subscription this message is associated with :param str transaction: include this nack in a named transaction :param str receipt: the receipt id :param keyword_headers: any additional headers to send with the nack command """ assert id is not None, "'id' is required" assert subscription is not None, "'subscription' is required" headers = {HDR_MESSAGE_ID: id, HDR_SUBSCRIPTION: subscription} headers = utils.merge_headers([headers, keyword_headers]) if transaction: headers[HDR_TRANSACTION] = transaction if receipt: headers[HDR_RECEIPT] = receipt self.send_frame(CMD_NACK, headers) def send(self, destination, body, content_type=None, headers=None, **keyword_headers): """ Send a message to a destination in the messaging system (as per https://stomp.github.io/stomp-specification-1.2.html#SEND) :param str destination: the destination (such as a message queue - for example '/queue/test' - or a message topic) :param body: the content of the message :param str content_type: the MIME type of message :param dict headers: additional headers to send in the message frame :param keyword_headers: any additional headers the broker requires """ assert destination is not None, "'destination' is required" assert body is not None, "'body' is required" headers = utils.merge_headers([headers, keyword_headers]) headers[HDR_DESTINATION] = destination if content_type: headers[HDR_CONTENT_TYPE] = content_type if self.auto_content_length and body and HDR_CONTENT_LENGTH not in headers: headers[HDR_CONTENT_LENGTH] = len(body) self.send_frame(CMD_SEND, headers, body) def subscribe(self, destination, id, ack="auto", headers=None, **keyword_headers): """ Subscribe to a destination :param str destination: the topic or queue to subscribe to :param str id: the identifier to uniquely identify the subscription :param str ack: either auto, client or client-individual (see https://stomp.github.io/stomp-specification-1.2.html#SUBSCRIBE for more info) :param dict headers: a map of any additional headers to send with the subscription :param keyword_headers: any additional headers to send with the subscription """ assert destination is not None, "'destination' is required" assert id is not None, "'id' is required" headers = utils.merge_headers([headers, keyword_headers]) headers[HDR_DESTINATION] = destination headers[HDR_ID] = id headers[HDR_ACK] = ack self.send_frame(CMD_SUBSCRIBE, headers) def unsubscribe(self, id, headers=None, **keyword_headers): """ Unsubscribe from a destination by its unique identifier :param str id: the unique identifier to unsubscribe from :param dict headers: additional headers to send with the unsubscribe :param keyword_headers: any additional headers to send with the subscription """ assert id is not None, "'id' is required" headers = utils.merge_headers([headers, keyword_headers]) headers[HDR_ID] = id self.send_frame(CMD_UNSUBSCRIBE, headers) class Protocol12(Protocol11): """ Represents version 1.2 of the protocol (see https://stomp.github.io/stomp-specification-1.2.html). Most users should not instantiate the protocol directly. See :py:mod:`stomp.connect` for connection classes. :param transport: :param (int,int) heartbeats: :param bool auto_content_length: Whether to calculate and send the content-length header automatically if it has not been set :param float heart_beat_receive_scale: how long to wait for a heartbeat before timing out, as a scale factor of receive time """ def __init__(self, transport, heartbeats=(0, 0), auto_content_length=True, heart_beat_receive_scale=1.5): Protocol11.__init__(self, transport, heartbeats, auto_content_length, heart_beat_receive_scale=heart_beat_receive_scale) self.version = "1.2" def _escape_headers(self, headers): """ :param dict(str,str) headers: """ for key, val in headers.items(): try: val = val.replace("\\", "\\\\").replace("\n", "\\n").replace(":", "\\c").replace("\r", "\\r") except: pass headers[key] = val def ack(self, id, transaction=None, receipt=None): """ Acknowledge 'consumption' of a message by id. :param str id: identifier of the message :param str transaction: include the acknowledgement in the specified transaction :param str receipt: the receipt id """ assert id is not None, "'id' is required" headers = {HDR_ID: id} if transaction: headers[HDR_TRANSACTION] = transaction if receipt: headers[HDR_RECEIPT] = receipt self.send_frame(CMD_ACK, headers) def nack(self, id, transaction=None, receipt=None, **keyword_headers): """ Let the server know that a message was not consumed. :param str id: the unique id of the message to nack :param str transaction: include this nack in a named transaction :param str receipt: the receipt id :param keyword_headers: any additional headers to send with the nack command """ assert id is not None, "'id' is required" headers = {HDR_ID: id} headers = utils.merge_headers([headers, keyword_headers]) if transaction: headers[HDR_TRANSACTION] = transaction if receipt: headers[HDR_RECEIPT] = receipt self.send_frame(CMD_NACK, headers) def connect(self, username=None, passcode=None, wait=False, headers=None, with_connect_command=False, **keyword_headers): """ Send a STOMP CONNECT frame. Differs from 1.0 and 1.1 versions in that the HOST header is enforced. :param str username: optionally specify the login user :param str passcode: optionally specify the user password :param bool wait: wait for the connection to complete before returning :param dict headers: a map of any additional headers to send with the subscription :param with_connect_command: if True, use CONNECT command instead of STOMP :param keyword_headers: any additional headers to send with the subscription """ cmd = CMD_CONNECT if with_connect_command else CMD_STOMP headers = utils.merge_headers([headers, keyword_headers]) headers[HDR_ACCEPT_VERSION] = self.version headers[HDR_HOST] = self.transport.current_host_and_port[0] if self.transport.vhost: headers[HDR_HOST] = self.transport.vhost if username is not None: headers[HDR_LOGIN] = username if passcode is not None: headers[HDR_PASSCODE] = passcode self.send_frame(cmd, headers) if wait: self.transport.wait_for_connection() if self.transport.connection_error: raise ConnectFailedException() stomp.py-8.1.0/stomp/transport.py000066400000000000000000001045371440306521300170710ustar00rootroot00000000000000"""Provides the underlying transport functionality (for stomp message transmission) - (mostly) independent from the actual STOMP protocol """ import errno import math import random import sys import time from io import BytesIO from time import monotonic try: from socket import SOL_SOCKET, SO_KEEPALIVE, SOL_TCP, TCP_KEEPIDLE, TCP_KEEPINTVL, TCP_KEEPCNT LINUX_KEEPALIVE_AVAIL = True except ImportError: LINUX_KEEPALIVE_AVAIL = False try: from socket import IPPROTO_TCP MAC_KEEPALIVE_AVAIL = True except ImportError: MAC_KEEPALIVE_AVAIL = False try: import ssl from ssl import SSLError DEFAULT_SSL_VERSION = ssl.PROTOCOL_TLS_CLIENT except (ImportError, AttributeError): ssl = None class SSLError(object): pass DEFAULT_SSL_VERSION = None import stomp.exception as exception import stomp.listener from stomp.utils import * from stomp import logging class BaseTransport(stomp.listener.Publisher): """ Base class for transport classes providing support for listeners, threading overrides, and anything else outside of actually establishing a network connection, sending and receiving of messages (so generally socket-agnostic functions). :param bool auto_decode: automatically decode message responses as strings, rather than leaving them as bytes. This preserves the behaviour as of version 4.0.16. (To be defaulted to False as of the next release) :param encoding: the character encoding to use for the message body """ # # Used to parse the STOMP "content-length" header lines, # __content_length_re = re.compile(b"^content-length[:]\\s*(?P[0-9]+)", re.MULTILINE) def __init__(self, auto_decode=True, encoding="utf-8", is_eol_fc=is_eol_default): self.__recvbuf = b"" self.listeners = {} self.running = False self.blocking = None self.connected = False self.connection_error = False self.disconnecting = False self.__receipts = {} self.current_host_and_port = None self.bind_host_port = None # flag used when we receive the disconnect receipt self.__disconnect_receipt = None self.notified_on_disconnect = False # function for creating threads used by the connection self.create_thread_fc = default_create_thread self.__listeners_change_condition = threading.Condition() self.__receiver_thread_exit_condition = threading.Condition() self.__receiver_thread_exited = False self.__send_wait_condition = threading.Condition() self.__connect_wait_condition = threading.Condition() self.__auto_decode = auto_decode self.__encoding = encoding self.__is_eol = is_eol_fc def override_threading(self, create_thread_fc): """ Override for thread creation. Use an alternate threading library by setting this to a function with a single argument (which is the receiver loop callback). The thread which is returned should be started (ready to run) :param function create_thread_fc: single argument function for creating a thread """ self.create_thread_fc = create_thread_fc # # Manage the connection # def start(self): """ Start the connection. This should be called after all listeners have been registered. If this method is not called, no frames will be received by the connection and no SSL/TLS handshake will occur. """ self.running = True self.attempt_connection() receiver_thread = self.create_thread_fc(self.__receiver_loop) logging.debug("created thread %s using func %s", receiver_thread, self.create_thread_fc) self.notify("connecting") def stop(self): """ Stop the connection. Performs a clean shutdown by waiting for the receiver thread to exit. """ with self.__receiver_thread_exit_condition: while not self.__receiver_thread_exited and self.is_connected(): self.__receiver_thread_exit_condition.wait() def is_connected(self): """ :rtype: bool """ return self.connected def set_connected(self, connected): """ :param bool connected: """ with self.__connect_wait_condition: self.connected = connected if connected: self.disconnecting = False self.__connect_wait_condition.notify() def set_receipt(self, receipt_id, value): if value: self.__receipts[receipt_id] = value elif receipt_id in self.__receipts: del self.__receipts[receipt_id] # # Manage objects listening to incoming frames # def set_listener(self, name, listener): """ Set a named listener to use with this connection. See :py:class:`stomp.listener.ConnectionListener` :param str name: the name of the listener :param ConnectionListener listener: the listener object """ assert listener is not None with self.__listeners_change_condition: self.listeners[name] = listener def remove_listener(self, name): """ Remove a listener according to the specified name :param str name: the name of the listener to remove """ with self.__listeners_change_condition: del self.listeners[name] def get_listener(self, name): """ Return the named listener :param str name: the listener to return :rtype: ConnectionListener """ return self.listeners.get(name) def process_frame(self, f, frame_str): """ :param Frame f: Frame object :param bytes frame_str: raw frame content """ frame_type = f.cmd.lower() if frame_type in ["connected", "message", "receipt", "error", "heartbeat"]: if frame_type == "message": self.notify("before_message", f) if logging.isEnabledFor(logging.DEBUG): logging.debug("received frame: %r, headers=%r, body=%r", f.cmd, f.headers, f.body) self.notify(frame_type, f) else: logging.warning("unknown response frame type: '%s' (frame length was %d)", frame_type, length(frame_str)) def notify(self, frame_type, frame=None): """ Utility function for notifying listeners of incoming and outgoing messages :param str frame_type: the type of message :param Frame frame: the stomp frame """ if frame_type == "receipt": # logic for wait-on-receipt notification receipt = frame.headers["receipt-id"] receipt_value = self.__receipts.get(receipt) with self.__send_wait_condition: self.set_receipt(receipt, None) self.__send_wait_condition.notify() if receipt_value == CMD_DISCONNECT: self.set_connected(False) # received a stomp 1.1+ disconnect receipt if receipt == self.__disconnect_receipt: self.disconnect_socket() self.__disconnect_receipt = None elif frame_type == "connected": self.set_connected(True) elif frame_type == "disconnected": self.notified_on_disconnect = True self.set_connected(False) with self.__listeners_change_condition: listeners = sorted(self.listeners.items()) for (_, listener) in listeners: notify_func = getattr(listener, "on_%s" % frame_type, None) if not notify_func: logging.debug("listener %s has no method on_%s", listener, frame_type) continue if frame_type in ("heartbeat", "disconnected"): notify_func() continue if frame_type == "connecting": notify_func(self.current_host_and_port) continue if frame_type == "error" and not self.connected: with self.__connect_wait_condition: self.connection_error = True self.__connect_wait_condition.notify() notify_func(frame) def transmit(self, frame): """ Convert a frame object to a frame string and transmit to the server. :param Frame frame: the Frame object to transmit """ with self.__listeners_change_condition: listeners = sorted(self.listeners.items()) for (_, listener) in listeners: try: listener.on_send(frame) except AttributeError: continue if frame.cmd == CMD_DISCONNECT and HDR_RECEIPT in frame.headers: self.__disconnect_receipt = frame.headers[HDR_RECEIPT] lines = convert_frame(frame) packed_frame = pack(lines) if logging.isEnabledFor(logging.DEBUG): logging.debug("sending frame: %s", clean_lines(lines)) self.send(packed_frame) def send(self, encoded_frame): """ Send an encoded frame over this transport (to be implemented in subclasses) :param bytes encoded_frame: a Frame object which has been encoded for transmission """ pass def receive(self): """ Receive a chunk of data (to be implemented in subclasses) :rtype: bytes """ pass def cleanup(self): """ Cleanup the transport (to be implemented in subclasses) """ pass def attempt_connection(self): """ Attempt to establish a connection. """ pass def disconnect_socket(self): """ Disconnect the socket. """ pass def wait_for_connection(self, timeout=None): """ Wait until we've established a connection with the server. :param float timeout: how long to wait, in seconds """ if timeout is not None: wait_time = timeout / 10.0 else: wait_time = None with self.__connect_wait_condition: while self.running and not self.is_connected() and not self.connection_error: self.__connect_wait_condition.wait(wait_time) if not self.running or not self.is_connected(): raise exception.ConnectFailedException() def __receiver_loop(self): """ Main loop listening for incoming data. """ logging.debug("starting receiver loop (%s)", threading.current_thread()) notify_disconnected = True try: while self.running: try: while self.running: frames = self.__read() for frame in frames: if self.__is_eol(frame): f = HEARTBEAT_FRAME else: f = parse_frame(frame) if f is None: continue if self.__auto_decode: f.body = decode(f.body) self.process_frame(f, frame) except exception.ConnectionClosedException: if self.running: # # Clear out any half-received messages after losing connection # self.__recvbuf = b"" self.running = False notify_disconnected = True break finally: self.cleanup() finally: with self.__receiver_thread_exit_condition: self.__receiver_thread_exited = True self.__receiver_thread_exit_condition.notify_all() logging.debug("receiver loop ended") self.notify("receiver_loop_completed") if notify_disconnected and not self.notified_on_disconnect: self.notify("disconnected") with self.__connect_wait_condition: self.__connect_wait_condition.notify_all() self.notified_on_disconnect = False def __read(self): """ Read the next frame(s) from the socket. :return: list of frames read :rtype: list(bytes) """ fastbuf = BytesIO() while self.running: try: try: c = self.receive() except exception.InterruptedException: logging.debug("socket read interrupted, restarting") continue except Exception: logging.debug("socket read error", exc_info=logging.verbose) c = b"" if c is None or len(c) == 0: logging.debug("nothing received, raising CCE") raise exception.ConnectionClosedException() if self.__is_eol(c) and not self.__recvbuf and not fastbuf.tell(): # # EOL to an empty receive buffer: treat as heartbeat. # Note that this may misdetect an optional EOL at end of frame as heartbeat in case the # previous receive() got a complete frame whose NUL at end of frame happened to be the # last byte of that read. But that should be harmless in practice. # fastbuf.close() return [c] fastbuf.write(c) if b"\x00" in c: # # Possible end of frame # break self.__recvbuf += fastbuf.getvalue() fastbuf.close() result = [] if self.__recvbuf and self.running: while True: pos = self.__recvbuf.find(b"\x00") if pos >= 0: frame = self.__recvbuf[0:pos] preamble_end_match = PREAMBLE_END_RE.search(frame) if preamble_end_match: preamble_end = preamble_end_match.start() content_length_match = BaseTransport.__content_length_re.search(frame[0:preamble_end]) if content_length_match: content_length = int(content_length_match.group("value")) content_offset = preamble_end_match.end() frame_size = content_offset + content_length if frame_size > len(frame): # # Frame contains NUL bytes, need to read more # if frame_size < len(self.__recvbuf): pos = frame_size frame = self.__recvbuf[0:pos] else: # # Haven't read enough data yet, exit loop and wait for more to arrive # break result.append(frame) pos += 1 # # Ignore optional EOLs at end of frame # while 1: if self.__is_eol(self.__recvbuf[pos:pos + 1]): pos += 1 elif self.__is_eol(self.__recvbuf[pos:pos + 2]): pos += 2 else: break self.__recvbuf = self.__recvbuf[pos:] else: break return result class Transport(BaseTransport): """ Represents a STOMP client 'transport'. Effectively this is the communications mechanism without the definition of the protocol. :param list((str,int)) host_and_ports: a list of (host, port) tuples :param bool prefer_localhost: if True and the local host is mentioned in the (host, port) tuples, try to connect to this first :param bool try_loopback_connect: if True and the local host is found in the host tuples, try connecting to it using loopback interface (127.0.0.1) :param float reconnect_sleep_initial: initial delay in seconds to wait before reattempting to establish a connection if connection to any of the hosts fails. :param float reconnect_sleep_increase: factor by which the sleep delay is increased after each connection attempt. For example, 0.5 means to wait 50% longer than before the previous attempt, 1.0 means wait twice as long, and 0.0 means keep the delay constant. :param float reconnect_sleep_max: maximum delay between connection attempts, regardless of the reconnect_sleep_increase. :param float reconnect_sleep_jitter: random additional time to wait (as a percentage of the time determined using the previous parameters) between connection attempts in order to avoid stampeding. For example, a value of 0.1 means to wait an extra 0%-10% (randomly determined) of the delay calculated using the previous three parameters. :param int reconnect_attempts_max: maximum attempts to reconnect (Can also be used for infinite attempts : `-1`) :param timeout: the timeout value to use when connecting the stomp socket :param keepalive: some operating systems support sending the occasional heart beat packets to detect when a connection fails. This parameter can either be set set to a boolean to turn on the default keepalive options for your OS, or as a tuple of values, which also enables keepalive packets, but specifies options specific to your OS implementation. For linux, supply ("linux", ka_idle, ka_intvl, ka_cnt) For macos, supply ("mac", ka_intvl) :param str vhost: specify a virtual hostname to provide in the 'host' header of the connection :param int recv_bytes: the number of bytes to use when calling recv """ def __init__(self, host_and_ports=None, prefer_localhost=True, try_loopback_connect=True, reconnect_sleep_initial=0.1, reconnect_sleep_increase=0.5, reconnect_sleep_jitter=0.1, reconnect_sleep_max=60.0, reconnect_attempts_max=3, timeout=None, keepalive=None, vhost=None, auto_decode=True, encoding="utf-8", recv_bytes=1024, is_eol_fc=is_eol_default, bind_host_port=None): BaseTransport.__init__(self, auto_decode, encoding, is_eol_fc) if host_and_ports is None: logging.debug("no hosts_and_ports specified, adding default localhost") host_and_ports = [("localhost", 61613)] sorted_host_and_ports = [] sorted_host_and_ports.extend(host_and_ports) # # If localhost is preferred, make sure all (host, port) tuples that refer to the local host come first in # the list # if prefer_localhost: sorted_host_and_ports.sort(key=is_localhost) # # If the user wishes to attempt connecting to local ports using the loopback interface, for each (host, port) # tuple referring to a local host, add an entry with the host name replaced by 127.0.0.1 if it doesn't # exist already # loopback_host_and_ports = [] if try_loopback_connect: for host_and_port in sorted_host_and_ports: if is_localhost(host_and_port) == 1: port = host_and_port[1] if not (("127.0.0.1", port) in sorted_host_and_ports or ( "localhost", port) in sorted_host_and_ports): loopback_host_and_ports.append(("127.0.0.1", port)) # # Assemble the final, possibly sorted list of (host, port) tuples # self.__host_and_ports = [] self.__host_and_ports.extend(loopback_host_and_ports) self.__host_and_ports.extend(sorted_host_and_ports) self.__bind_host_port = bind_host_port self.__reconnect_sleep_initial = reconnect_sleep_initial self.__reconnect_sleep_increase = reconnect_sleep_increase self.__reconnect_sleep_jitter = reconnect_sleep_jitter self.__reconnect_sleep_max = reconnect_sleep_max self.__reconnect_attempts_max = reconnect_attempts_max self.__timeout = timeout self.socket = None self.__socket_semaphore = threading.BoundedSemaphore(1) self.current_host_and_port = None # setup SSL self.__ssl_params = {} self.__keepalive = keepalive self.vhost = vhost self.__recv_bytes = recv_bytes def is_connected(self): """ Return true if the socket managed by this connection is connected :rtype: bool """ try: return self.socket is not None and self.socket.getsockname()[1] != 0 and BaseTransport.is_connected(self) except socket.error: return False def disconnect_socket(self): """ Disconnect the underlying socket connection """ self.running = False if self.socket is not None: if self.__need_ssl(): # # Even though we don't want to use the socket, unwrap is the only API method which does a proper SSL # shutdown # try: self.socket = self.socket.unwrap() except Exception: # # unwrap seems flaky on Win with the back-ported ssl mod, so catch any exception and log it # _, e, _ = sys.exc_info() logging.warning(e) elif hasattr(socket, "SHUT_RDWR"): try: self.socket.shutdown(socket.SHUT_RDWR) except socket.error: _, e, _ = sys.exc_info() # ignore when socket already closed if get_errno(e) != errno.ENOTCONN: logging.warning("unable to issue SHUT_RDWR on socket because of error '%s'", e) # # split this into a separate check, because sometimes the socket is nulled between shutdown and this call # if self.socket is not None: try: self.socket.close() except socket.error: _, e, _ = sys.exc_info() logging.warning("unable to close socket because of error '%s'", e) self.current_host_and_port = None self.socket = None if not self.notified_on_disconnect: self.notify("disconnected") def send(self, encoded_frame): """ :param bytes encoded_frame: """ if self.socket is not None: try: with self.__socket_semaphore: self.socket.sendall(encoded_frame) except Exception: _, e, _ = sys.exc_info() logging.error("error sending frame", exc_info=True) raise e else: raise exception.NotConnectedException() def receive(self): """ :rtype: bytes """ try: return self.socket.recv(self.__recv_bytes) except socket.error: _, e, _ = sys.exc_info() if get_errno(e) in (errno.EAGAIN, errno.EINTR): logging.debug("socket read interrupted, restarting") raise exception.InterruptedException() if self.is_connected(): raise def cleanup(self): """ Close the socket and clear the current host and port details. """ try: self.socket.close() except: pass # ignore errors when attempting to close socket self.socket = None def __enable_keepalive(self): def try_setsockopt(sock, name, fam, opt, val=None): if val is None: return True # no value to set always works try: sock.setsockopt(fam, opt, val) logging.debug("keepalive: set %r option to %r on socket", name, val) except: logging.error("keepalive: unable to set %r option to %r on socket", name, val) return False return True ka = self.__keepalive if not ka: return if ka is True: ka_sig = "auto" ka_args = () else: try: ka_sig = ka[0] ka_args = ka[1:] except Exception: logging.error("keepalive: bad specification %r", ka) return if ka_sig == "auto": if LINUX_KEEPALIVE_AVAIL: ka_sig = "linux" ka_args = None logging.debug("keepalive: autodetected linux-style support") elif MAC_KEEPALIVE_AVAIL: ka_sig = "mac" ka_args = None logging.debug("keepalive: autodetected mac-style support") else: logging.error("keepalive: unable to detect any implementation, DISABLED!") return if ka_sig == "linux": logging.debug("keepalive: activating linux-style support") if ka_args is None: logging.debug("keepalive: using system defaults") ka_args = (None, None, None) ka_idle, ka_intvl, ka_cnt = ka_args if try_setsockopt(self.socket, "enable", SOL_SOCKET, SO_KEEPALIVE, 1): try_setsockopt(self.socket, "idle time", SOL_TCP, TCP_KEEPIDLE, ka_idle) try_setsockopt(self.socket, "interval", SOL_TCP, TCP_KEEPINTVL, ka_intvl) try_setsockopt(self.socket, "count", SOL_TCP, TCP_KEEPCNT, ka_cnt) elif ka_sig == "mac": logging.debug("keepalive: activating mac-style support") if ka_args is None: logging.debug("keepalive: using system defaults") ka_args = (3,) ka_intvl = ka_args if try_setsockopt(self.socket, "enable", SOL_SOCKET, SO_KEEPALIVE, 1): try_setsockopt(self.socket, socket.IPPROTO_TCP, 0x10, ka_intvl) else: logging.error("keepalive: implementation %r not recognized or not supported", ka_sig) def attempt_connection(self): """ Try connecting to the (host, port) tuples specified at construction time. """ self.connection_error = False sleep_exp = 1 connect_count = 0 logging.debug("attempt reconnection (%s, %s, %s)", self.running, self.socket, connect_count) while self.running and self.socket is None and (connect_count < self.__reconnect_attempts_max or self.__reconnect_attempts_max == -1): for host_and_port in self.__host_and_ports: try: logging.debug("attempting connection to host %s, port %s", host_and_port[0], host_and_port[1]) if self.__bind_host_port: self.socket = socket.create_connection(host_and_port, self.__timeout, self.__bind_host_port) else: self.socket = socket.create_connection(host_and_port, self.__timeout) self.__enable_keepalive() need_ssl = self.__need_ssl(host_and_port) if need_ssl: # wrap socket ssl_params = self.get_ssl(host_and_port) tls_context = ssl.SSLContext(DEFAULT_SSL_VERSION) if ssl_params["ca_certs"]: cert_validation = ssl.CERT_REQUIRED tls_context.load_verify_locations(ssl_params["ca_certs"]) else: cert_validation = ssl.CERT_NONE if tls_context: # Wrap the socket for TLS certfile = ssl_params["cert_file"] keyfile = ssl_params["key_file"] password = ssl_params.get("password") if certfile and not keyfile: keyfile = certfile if certfile: tls_context.load_cert_chain(certfile, keyfile, password) if cert_validation is None or cert_validation == ssl.CERT_NONE: tls_context.check_hostname = False tls_context.verify_mode = cert_validation logging.debug("wrapping SSL socket") self.socket = tls_context.wrap_socket(self.socket, server_hostname=host_and_port[0]) else: # Old-style wrap_socket where we don't have a modern SSLContext (so no SNI) logging.debug("wrapping SSL socket (old style)") self.socket = ssl.wrap_socket( self.socket, keyfile=ssl_params["key_file"], certfile=ssl_params["cert_file"], cert_reqs=cert_validation, ca_certs=ssl_params["ca_certs"], ssl_version=ssl_params["ssl_version"]) self.socket.settimeout(self.__timeout) if self.blocking is not None: self.socket.setblocking(self.blocking) # # Validate server cert # if need_ssl and ssl_params["cert_validator"]: cert = self.socket.getpeercert() (ok, errmsg) = ssl_params["cert_validator"](cert, host_and_port[0]) if not ok: raise SSLError("Server certificate validation failed: %s", errmsg) self.current_host_and_port = host_and_port logging.info("established connection to host %s, port %s", host_and_port[0], host_and_port[1]) break except (OSError, AssertionError): self.socket = None connect_count += 1 logging.warning("could not connect to host %s, port %s", host_and_port[0], host_and_port[1], exc_info=logging.verbose) if self.socket is None: sleep_duration = (min(self.__reconnect_sleep_max, ((self.__reconnect_sleep_initial / (1.0 + self.__reconnect_sleep_increase)) * math.pow(1.0 + self.__reconnect_sleep_increase, sleep_exp))) * (1.0 + random.random() * self.__reconnect_sleep_jitter)) sleep_end = monotonic() + sleep_duration logging.debug("sleeping for %.1f seconds before attempting reconnect", sleep_duration) while self.running and monotonic() < sleep_end: time.sleep(0.2) if sleep_duration < self.__reconnect_sleep_max: sleep_exp += 1 if not self.socket: raise exception.ConnectFailedException() def set_ssl(self, for_hosts=[], key_file=None, cert_file=None, ca_certs=None, cert_validator=None, ssl_version=DEFAULT_SSL_VERSION, password=None): """ Sets up SSL configuration for the given hosts. This ensures socket is wrapped in a SSL connection, raising an exception if the SSL module can't be found. :param for_hosts: a list of tuples describing hosts this SSL configuration should be applied to :param cert_file: the path to a X509 certificate :param key_file: the path to a X509 key file :param ca_certs: the path to the a file containing CA certificates to validate the server against. If this is not set, server side certificate validation is not done. :param cert_validator: function which performs extra validation on the client certificate, for example checking the returned certificate has a commonName attribute equal to the hostname (to avoid man in the middle attacks). The signature is: (OK, err_msg) = validation_function(cert, hostname) where OK is a boolean, and cert is a certificate structure as returned by ssl.SSLSocket.getpeercert() :param ssl_version: SSL protocol to use for the connection. This should be one of the PROTOCOL_x constants provided by the ssl module. The default is ssl.PROTOCOL_TLSv1 :param password: SSL password """ if not ssl: raise Exception("SSL connection requested, but SSL library not found") for host_port in for_hosts: self.__ssl_params[host_port] = dict(key_file=key_file, cert_file=cert_file, ca_certs=ca_certs, cert_validator=cert_validator, ssl_version=ssl_version, password=password) def __need_ssl(self, host_and_port=None): """ Whether current host needs SSL or not. :param (str,int) host_and_port: the host/port pair to check, default current_host_and_port """ if not host_and_port: host_and_port = self.current_host_and_port return host_and_port in self.__ssl_params def get_ssl(self, host_and_port=None): """ Get SSL params for the given host. :param (str,int) host_and_port: the host/port pair we want SSL params for, default current_host_and_port """ if not host_and_port: host_and_port = self.current_host_and_port return self.__ssl_params.get(host_and_port) stomp.py-8.1.0/stomp/utils.py000066400000000000000000000205111440306521300161620ustar00rootroot00000000000000"""General utility functions. """ import copy import os import re import socket import threading import uuid from stomp.constants import * # List of all host names (unqualified, fully-qualified, and IP # addresses) that refer to the local host (both loopback interface # and external interfaces). This is used for determining # preferred targets. LOCALHOST_NAMES = ["localhost", "127.0.0.1"] NULL = b"\x00" if not os.environ.get("STOMP_SKIP_HOSTNAME_SCAN"): try: LOCALHOST_NAMES.append(socket.gethostbyname(socket.gethostname())) except Exception: pass try: LOCALHOST_NAMES.append(socket.gethostname()) except Exception: pass try: LOCALHOST_NAMES.append(socket.getfqdn(socket.gethostname())) except Exception: pass def decode(byte_data, encoding="utf-8"): """ Decode the byte data to a string if not None. :param bytes byte_data: the data to decode :param string encoding: character encoding :rtype: str """ if byte_data is None: return None return byte_data.decode(encoding, errors="replace") def encode(char_data, encoding="utf-8"): """ Encode the parameter as a byte string. :param char_data: the data to encode :param string encoding: character encoding :rtype: bytes """ if type(char_data) is str: return char_data.encode(encoding, errors="replace") elif type(char_data) is bytes: return char_data else: raise TypeError("message should be a string or bytes, found %s" % type(char_data)) def pack(pieces=()): """ Join a sequence of strings together. :param list pieces: list of strings :rtype: bytes """ return b"".join(pieces) def join(chars=()): """ Join a sequence of characters into a string. :param bytes chars: list of chars :rtype: str """ return b"".join(chars).decode() def is_eol_default(c): return c == b"\x0a" ## # Used to parse STOMP header lines in the format "key:value", # HEADER_LINE_RE = re.compile("(?P[^:]+)[:](?P.*)") ## # As of STOMP 1.2, lines can end with either line feed, or carriage return plus line feed. # PREAMBLE_END_RE = re.compile(b"\n\n|\r\n\r\n") ## # As of STOMP 1.2, lines can end with either line feed, or carriage return plus line feed. # LINE_END_RE = re.compile("\n|\r\n") ## # Used to replace the "passcode" to be dumped in the transport log (at debug level) # PASSCODE_RE = re.compile(r"passcode:\s+[^\' ]+") ENC_NEWLINE = encode("\n") ENC_NULL = encode(NULL) def default_create_thread(callback): """ Default thread creation - used to create threads when the client doesn't want to provide their own thread creation. :param function callback: the callback function provided to threading.Thread """ thread = threading.Thread(None, callback) thread.daemon = True # Don't let thread prevent termination thread.start() return thread def is_localhost(host_and_port): """ Return 1 if the specified host+port is a member of the 'localhost' list of hosts, 2 if not (predominately used as a sort key. :param (str,int) host_and_port: tuple containing host and port :rtype: int """ (host, _) = host_and_port if host in LOCALHOST_NAMES: return 1 return 2 _HEADER_ESCAPES = { "\r": "\\r", "\n": "\\n", ":": "\\c", "\\": "\\\\", } _HEADER_UNESCAPES = dict((value, key) for (key, value) in _HEADER_ESCAPES.items()) def _unescape_header(matchobj): escaped = matchobj.group(0) unescaped = _HEADER_UNESCAPES.get(escaped) if not unescaped: # TODO: unknown escapes MUST be treated as fatal protocol error per spec unescaped = escaped return unescaped def parse_headers(lines, offset=0): """ Parse the headers in a STOMP response :param list(str) lines: the lines received in the message response :param int offset: the starting line number :rtype: dict(str,str) """ headers = {} for header_line in lines[offset:]: header_match = HEADER_LINE_RE.match(header_line) if header_match: key = header_match.group("key") key = re.sub(r"\\.", _unescape_header, key) if key not in headers: value = header_match.group("value") value = re.sub(r"\\.", _unescape_header, value) headers[key] = value return headers def parse_frame(frame): """ Parse a STOMP frame into a Frame object. :param bytes frame: the frame received from the server (as a byte string) :rtype: Frame """ mat = PREAMBLE_END_RE.search(frame) if mat: preamble_end = mat.start() body_start = mat.end() else: preamble_end = len(frame) body_start = preamble_end preamble = decode(frame[0:preamble_end]) preamble_lines = LINE_END_RE.split(preamble) preamble_len = len(preamble_lines) body = frame[body_start:] # Skip any leading newlines first_line = 0 while first_line < preamble_len and len(preamble_lines[first_line]) == 0: first_line += 1 if first_line >= preamble_len: return None # Extract frame type/command cmd = preamble_lines[first_line] # Put headers into a key/value map headers = parse_headers(preamble_lines, first_line + 1) return Frame(cmd, headers, body) def merge_headers(header_map_list): """ Helper function for combining multiple header maps into one. :param list(dict) header_map_list: list of maps :rtype: dict """ headers = {} for header_map in header_map_list: if header_map: headers.update(header_map) return headers def clean_headers(headers): rtn = headers if "passcode" in headers: rtn = copy.copy(headers) rtn["passcode"] = "********" return rtn # lines: lines returned from a call to convert_frames def clean_lines(lines): return re.sub(PASSCODE_RE, "passcode:********", str(lines)) def calculate_heartbeats(shb, chb): """ Given a heartbeat string from the server, and a heartbeat tuple from the client, calculate what the actual heartbeat settings should be. :param (str,str) shb: server heartbeat numbers :param (int,int) chb: client heartbeat numbers :rtype: (int,int) """ (sx, sy) = shb (cx, cy) = chb x = 0 y = 0 if cx != 0 and sy != "0": x = max(cx, int(sy)) if cy != 0 and sx != "0": y = max(cy, int(sx)) return x, y def convert_frame(frame): """ Convert a frame to a list of lines separated by newlines. :param Frame frame: the Frame object to convert :rtype: list(str) """ lines = [] body = None if frame.body: body = encode(frame.body) if HDR_CONTENT_LENGTH in frame.headers: frame.headers[HDR_CONTENT_LENGTH] = len(body) if frame.cmd: lines.append(encode(frame.cmd)) lines.append(ENC_NEWLINE) for key, vals in sorted(frame.headers.items()): if vals is None: continue if type(vals) != tuple: vals = (vals,) for val in vals: lines.append(encode("%s:%s\n" % (key, val))) lines.append(ENC_NEWLINE) if body: lines.append(body) if frame.cmd: lines.append(ENC_NULL) return lines def length(s): """ Null (none) safe length function. :param str s: the string to return length of (None allowed) :rtype: int """ if s is not None: return len(s) return 0 class Frame(object): """ A STOMP frame (or message). :param str cmd: the protocol command :param dict headers: a map of headers for the frame :param body: the content of the frame. """ def __init__(self, cmd, headers=None, body=None): self.cmd = cmd self.headers = headers if headers is not None else {} self.original_headers = copy.copy(self.headers) self.body = body def __str__(self): return "{cmd=%s,headers=[%s],body=%s}" % (self.cmd, self.headers, self.body) def get_uuid(): return str(uuid.uuid4()) def get_errno(e): """ Return the errno of an exception, or the first argument if errno is not available. :param Exception e: the exception object """ try: return e.errno except AttributeError: return e.args[0] HEARTBEAT_FRAME = Frame("heartbeat") stomp.py-8.1.0/tests/000077500000000000000000000000001440306521300144515ustar00rootroot00000000000000stomp.py-8.1.0/tests/__init__.py000066400000000000000000000000001440306521300165500ustar00rootroot00000000000000stomp.py-8.1.0/tests/haproxy.cfg000066400000000000000000000005461440306521300166310ustar00rootroot00000000000000defaults mode tcp option tcplog frontend ft_test bind 0.0.0.0:65001 ssl crt tmp/myorg.pem crt tmp/mycom.pem no-sslv3 no-tls-tickets use_backend bk_com_cert if { ssl_fc_sni my.example.com } use_backend bk_org_cert if { ssl_fc_sni my.example.org } backend bk_com_cert server srv1 127.0.0.1:62613 backend bk_org_cert server srv2 127.0.0.1:62614stomp.py-8.1.0/tests/setup.ini000066400000000000000000000007101440306521300163100ustar00rootroot00000000000000[default] host = 172.17.0.2 port = 62613 ssl_port = 62614 ssl_expired_port = 62619 user = admin password = password [ipv6] port = 62613 [rabbitmq] host = 172.17.0.2 port = 61613 user = guest password = guest [rabbitmq_ws] host = 172.17.0.2 port = 15674 user = guest password = guest [stompserver] host = 172.17.0.2 port = 63613 [sni] host = my.example.com ssl_port = 65001 [artemis] host = 172.17.0.2 port = 61615 user = testuser password = password stomp.py-8.1.0/tests/test-out.gif000066400000000000000000000000741440306521300167250ustar00rootroot00000000000000GIF87a !, `|"L ʨ8KL_;stomp.py-8.1.0/tests/test-out.gif.gz000066400000000000000000000001211440306521300173350ustar00rootroot00000000000000KVtest.gifst0Obbh`+da1u@HI1AF׶S+f[xgdŮE<stomp.py-8.1.0/tests/test.gif000066400000000000000000000000741440306521300161200ustar00rootroot00000000000000GIF87a !, `|"L ʨ8KL_;stomp.py-8.1.0/tests/test.gif.gz000066400000000000000000000001211440306521300165300ustar00rootroot00000000000000KVtest.gifst0Obbh`+da1u@HI1AF׶S+f[xgdŮE<stomp.py-8.1.0/tests/test_activemq.py000066400000000000000000000013161440306521300176740ustar00rootroot00000000000000import stomp from stomp.listener import TestListener from .testutils import * @pytest.fixture() def conn(): conn = stomp.Connection11(get_default_host()) conn.set_listener("testlistener", TestListener("123", print_to_log=True)) conn.connect(get_default_user(), get_default_password(), wait=True) yield conn conn.disconnect(receipt=None) class TestActiveMQ(object): def test_send_to_activemq(self, conn): conn.subscribe(destination="/queue/test", id=1, ack="auto") conn.send(body="this is a test", destination="/queue/test", content_type="text/blah", receipt="123") validate_send(conn) logging.info(conn.get_listener("testlistener").get_latest_message()) stomp.py-8.1.0/tests/test_artemis.py000066400000000000000000000023521440306521300175300ustar00rootroot00000000000000import stomp from stomp.listener import TestListener from .testutils import * @pytest.fixture() def conn(): conn = stomp.Connection11(get_artemis_host()) conn.set_listener("testlistener", TestListener("123", print_to_log=True)) conn.connect(get_artemis_user(), get_artemis_password(), wait=True) yield conn conn.disconnect(receipt=None) @pytest.fixture() def conn2(): conn2 = stomp.Connection11(get_artemis_host()) conn2.set_listener("testlistener", TestListener("456", print_to_log=True)) conn2.connect(get_artemis_user(), get_artemis_password(), wait=True, headers={'consumerWindowSize': 0}) yield conn2 conn2.disconnect(receipt=None) class TestArtemis(object): def test_send_to_artemis(self, conn): conn.subscribe(destination="/queue/test", id=1, ack="auto") conn.send(body="this is a test", destination="/queue/test", receipt="123") validate_send(conn) def test_prefetchsize(self, conn2): conn2.subscribe(destination="/queue/test2", id=2, ack="auto", headers={'consumerWindowSize': 0}) conn2.send(body="testing sending a message after subscribing with prefetch", destination="/queue/test2", receipt="456") validate_send(conn2) stomp.py-8.1.0/tests/test_basic.py000066400000000000000000000212751440306521300171520ustar00rootroot00000000000000import signal import socket from time import monotonic import stomp from stomp.listener import TestListener from .testutils import * @pytest.fixture() def testlistener(): yield TestListener("123", print_to_log=True) @pytest.fixture() def conn(testlistener): conn = stomp.Connection11(get_default_host()) conn.set_listener("testlistener", testlistener) conn.connect(get_default_user(), get_default_password(), wait=True) yield conn conn.disconnect(receipt=None) @pytest.fixture() def invalidconn(testlistener): conn = stomp.Connection([("192.0.2.0", 60000)], timeout=5, reconnect_attempts_max=1) conn.set_listener("testlistener", testlistener) yield conn @pytest.fixture() def ipv4only(monkeypatch): """ Filter DNS requests to return only IPv4 results. This is useful to avoid long timeouts when tests attempt to connect to the IPv6 address, but the service is only listening on the IPv4 address. """ real_getaddrinfo = socket.getaddrinfo def getaddrinfo_ipv4(*args, **kw): return [a for a in real_getaddrinfo(*args, **kw) if a[0] == socket.AF_INET] monkeypatch.setattr(socket, "getaddrinfo", getaddrinfo_ipv4) class TestBasic(object): def test_subscribe_and_send(self, conn, testlistener): queuename = "/queue/test1-%s" % testlistener.timestamp conn.subscribe(destination=queuename, id=1, ack="auto") conn.send(body='{"val": "this is a test"}', destination=queuename, content_type="application/json", receipt="123") validate_send(conn) (headers, body) = testlistener.get_latest_message() assert "content-type" in headers assert headers["content-type"] == "application/json" def test_default_to_localhost(self, ipv4only): conn = stomp.Connection() listener = TestListener("123", print_to_log=True) queuename = "/queue/test1-%s" % listener.timestamp conn.set_listener("testlistener", listener) conn.connect(get_rabbitmq_user(), get_rabbitmq_password(), wait=True) conn.send(body="this is a test", destination=queuename, receipt="123") conn.disconnect(receipt=None) def test_commit(self, conn): timestamp = time.strftime("%Y%m%d%H%M%S") queuename = "/queue/test2-%s" % timestamp conn.subscribe(destination=queuename, id=1, ack="auto") trans_id = conn.begin() conn.send(body="this is a test1", destination=queuename, transaction=trans_id) conn.send(body="this is a test2", destination=queuename, transaction=trans_id) conn.send(body="this is a test3", destination=queuename, transaction=trans_id, receipt="123") time.sleep(3) listener = conn.get_listener("testlistener") assert listener.connections == 1, "should have received 1 connection acknowledgement" assert listener.messages == 0, "should not have received any messages" conn.commit(transaction=trans_id) listener.wait_for_message() time.sleep(3) assert listener.messages == 3, "should have received 3 messages" assert listener.errors == 0, "should not have received any errors" def test_abort(self, conn): timestamp = time.strftime("%Y%m%d%H%M%S") queuename = "/queue/test3-%s" % timestamp conn.subscribe(destination=queuename, id=1, ack="auto") trans_id = conn.begin() conn.send(body="this is a test1", destination=queuename, transaction=trans_id) conn.send(body="this is a test2", destination=queuename, transaction=trans_id) conn.send(body="this is a test3", destination=queuename, transaction=trans_id) time.sleep(3) listener = conn.get_listener("testlistener") assert listener.connections == 1, "should have received 1 connection acknowledgement" assert listener.messages == 0, "should not have received any messages" conn.abort(transaction=trans_id) time.sleep(3) assert listener.messages == 0, "should not have received any messages" assert listener.errors == 0, "should not have received any errors" def test_timeout(self, invalidconn): ms = monotonic() try: invalidconn.connect("test", "test") pytest.fail("shouldn't happen") except stomp.exception.ConnectFailedException: pass # success! ms = monotonic() - ms assert ms > 5.0, "connection timeout should have been at least 5 seconds" def test_childinterrupt(self, conn): def childhandler(signum, frame): print("received child signal") oldhandler = signal.signal(signal.SIGCHLD, childhandler) timestamp = time.strftime("%Y%m%d%H%M%S") queuename = "/queue/test5-%s" % timestamp conn.subscribe(destination=queuename, id=1, ack="auto", receipt="123") listener = conn.get_listener("testlistener") listener.wait_on_receipt() conn.send(body="this is an interrupt test 1", destination=queuename) print("causing signal by starting child process") os.system("sleep 1") time.sleep(1) signal.signal(signal.SIGCHLD, oldhandler) print("completed signal section") conn.send(body="this is an interrupt test 2", destination=queuename, receipt="123") listener.wait_for_message() assert listener.connections == 1, "should have received 1 connection acknowledgment" assert listener.errors == 0, "should not have received any errors" assert conn.is_connected(), "should still be connected to STOMP provider" def test_clientack(self, conn): timestamp = time.strftime("%Y%m%d%H%M%S") queuename = "/queue/testclientack-%s" % timestamp conn.subscribe(destination=queuename, id=1, ack="client") conn.send(body="this is a test", destination=queuename, receipt="123") listener = conn.get_listener("testlistener") listener.wait_for_message() (headers, _) = listener.get_latest_message() message_id = headers["message-id"] subscription = headers["subscription"] conn.ack(message_id, subscription) def test_clientnack(self, conn): timestamp = time.strftime("%Y%m%d%H%M%S") queuename = "/queue/testclientnack-%s" % timestamp conn.subscribe(destination=queuename, id=1, ack="client") conn.send(body="this is a test", destination=queuename, receipt="123") listener = conn.get_listener("testlistener") listener.wait_for_message() (headers, _) = listener.get_latest_message() message_id = headers["message-id"] subscription = headers["subscription"] conn.nack(message_id, subscription) def test_specialchars(self, conn): timestamp = time.strftime("%Y%m%d%H%M%S") queuename = "/queue/testspecialchars-%s" % timestamp conn.subscribe(destination=queuename, id=1, ack="client") hdrs = { "special-1": "test with colon : test", "special-2": "test with backslash \\ test", "special-3": "test with newline \n" } conn.send(body="this is a test", headers=hdrs, destination=queuename, receipt="123") listener = conn.get_listener("testlistener") listener.wait_for_message() (headers, _) = listener.get_latest_message() _ = headers["message-id"] _ = headers["subscription"] assert "special-1" in headers assert "test with colon : test" == headers["special-1"] assert "special-2" in headers assert "test with backslash \\ test" == headers["special-2"] assert "special-3" in headers assert "test with newline \n" == headers["special-3"] def test_host_bind_port(self, ipv4only): conn = stomp.Connection(bind_host_port=("localhost", next_free_port())) listener = TestListener("981", print_to_log=True) queuename = "/queue/testbind-%s" % listener.timestamp conn.set_listener("testlistener", listener) conn.connect(get_rabbitmq_user(), get_rabbitmq_password(), wait=True) conn.send(body="this is a test using local bind port", destination=queuename, receipt="981") conn.disconnect(receipt=None) class TestConnectionErrors(object): def test_connect_wait_error(self): conn = stomp.Connection(get_default_host()) try: conn.connect("invalid", "user", True) pytest.fail("Shouldn't happen") except: pass def test_connect_nowait_error(self): conn = stomp.Connection(get_default_host()) try: conn.connect("invalid", "user", False) assert not conn.is_connected(), "Should not be connected" except: pytest.fail("Shouldn't happen") stomp.py-8.1.0/tests/test_cli.py000066400000000000000000000207311440306521300166340ustar00rootroot00000000000000import tempfile from stomp.__main__ import StompCLI from .testutils import * username = get_default_user() password = get_default_password() (host, port) = get_default_host()[0] test_text = '''subscribe /queue/testfile send /queue/testfile this is a test unsubscribe /queue/testfile''' def create_test_file(): with tempfile.NamedTemporaryFile('w', delete=False) as f: f.write('''subscribe /queue/testfile send /queue/testfile this is a test unsubscribe /queue/testfile''') return f class TestCLI(object): def test_invalid_version(self): with pytest.raises(RuntimeError): StompCLI(host, port, username, password, "invalid") def test_help(self): teststdin = StubStdin() cli = StompCLI(host, port, username, password, "1.0", stdin=teststdin) cli.help_help() cli.help_quit() cli.help_exit() cli.help_EOF() cli.help_subscribe() cli.help_unsubscribe() cli.help_send() cli.help_sendrec() cli.help_sendreply() cli.help_sendfile() cli.help_version() cli.help_ack() cli.help_nack() cli.help_abort() cli.help_commit() cli.help_stats() cli.help_run() cli.help_begin() def testsubscribe(self): teststdin = StubStdin() teststdout = StubStdout(self) teststdout.expect("CONNECTED") cli = StompCLI(host, port, username, password, "1.0", stdin=teststdin, stdout=teststdout) time.sleep(3) teststdout.expect("Subscribing to '/queue/testsubscribe' with acknowledge set to 'auto', id set to '1'") cli.onecmd("subscribe /queue/testsubscribe") teststdout.expect("MESSAGE") teststdout.expect("this is a test") cli.onecmd("send /queue/testsubscribe this is a test") time.sleep(3) teststdout.expect("Unsubscribing from '/queue/testsubscribe'") cli.onecmd("unsubscribe /queue/testsubscribe") teststdout.expect("Shutting down, please wait") cli.onecmd("quit") def testsendrec(self): teststdin = StubStdin() teststdout = StubStdout(self) teststdout.expect("CONNECTED") cli = StompCLI(host, port, username, password, "1.1", stdin=teststdin, stdout=teststdout) time.sleep(3) teststdout.expect("RECEIPT") cli.onecmd("sendrec /queue/testsendrec this is a test") time.sleep(3) teststdout.expect("Shutting down, please wait") cli.onecmd("quit") def testsendfile(self): f = create_test_file() teststdin = StubStdin() teststdout = StubStdout(self) teststdout.expect("CONNECTED") cli = StompCLI(host, port, username, password, "1.2", stdin=teststdin, stdout=teststdout) time.sleep(3) teststdout.expect("Subscribing to '/queue/testsendfile' with acknowledge set to 'auto', id set to '1'") cli.onecmd("subscribe /queue/testsendfile") time.sleep(3) teststdout.expect("MESSAGE") cli.onecmd("sendfile /queue/testsendfile %s" % f.name) time.sleep(3) teststdout.expect("Shutting down, please wait") cli.onecmd("quit") def testsendfileheaders(self): f = create_test_file() teststdin = StubStdin() teststdout = StubStdout(self) teststdout.expect("CONNECTED") cli = StompCLI(host, port, username, password, "1.2", stdin=teststdin, stdout=teststdout) time.sleep(3) cli.onecmd("sendfile /queue/testsendfile %s { \"custom\" : \"header\" }" % f.name) time.sleep(3) teststdout.expect("Shutting down, please wait") cli.onecmd("quit") def testabort(self): teststdin = StubStdin() teststdout = StubStdout(self) teststdout.expect("CONNECTED") cli = StompCLI(host, port, username, password, "1.2", stdin=teststdin, stdout=teststdout) time.sleep(3) teststdout.expect("Subscribing to '/queue/testabort' with acknowledge set to 'auto', id set to '1'") cli.onecmd("subscribe /queue/testabort") cli.onecmd("begin") cli.onecmd("send /queue/testabort this is a test") cli.onecmd("abort") time.sleep(3) teststdout.expect("Unsubscribing from '/queue/testabort'") cli.onecmd("unsubscribe /queue/testabort") teststdout.expect("Shutting down, please wait") cli.onecmd("quit") def testcommit(self): teststdin = StubStdin() teststdout = StubStdout(self) teststdout.expect("CONNECTED") cli = StompCLI(host, port, username, password, "1.0", stdin=teststdin, stdout=teststdout) time.sleep(3) teststdout.expect("Subscribing to '/queue/testcommit' with acknowledge set to 'auto', id set to '1'") cli.onecmd("subscribe /queue/testcommit") cli.onecmd("begin") cli.onecmd("send /queue/testcommit this is a test") teststdout.expect("Committing.*") teststdout.expect("MESSAGE") teststdout.expect("this is a test") cli.onecmd("commit") time.sleep(3) teststdout.expect("Unsubscribing from '/queue/testcommit'") cli.onecmd("unsubscribe /queue/testcommit") teststdout.expect("Shutting down, please wait") cli.onecmd("quit") def teststats(self): teststdin = StubStdin() teststdout = StubStdout(self) teststdout.expect("CONNECTED") cli = StompCLI(host, port, username, password, "1.0", stdin=teststdin, stdout=teststdout) time.sleep(3) teststdout.expect("Subscribing to '/queue/teststats' with acknowledge set to 'auto', id set to '1'") cli.onecmd("subscribe /queue/teststats") teststdout.expect(".*No stats available.*") cli.onecmd("stats") time.sleep(1) cli.onecmd("stats on") cli.onecmd("send /queue/teststats this is a test") teststdout.expect("MESSAGE") teststdout.expect("this is a test") time.sleep(3) cli.onecmd("stats") teststdout.expect("Unsubscribing from '/queue/teststats'") cli.onecmd("unsubscribe /queue/teststats") teststdout.expect("Shutting down, please wait") cli.onecmd("quit") def testrun(self): f = create_test_file() teststdin = StubStdin() teststdout = StubStdout(self) teststdout.expect("CONNECTED") cli = StompCLI(host, port, username, password, "1.0", stdin=teststdin, stdout=teststdout) time.sleep(3) teststdout.expect("Subscribing to '/queue/testfile' with acknowledge set to 'auto', id set to '1'") teststdout.expect("this is a test") teststdout.expect("MESSAGE") teststdout.expect("Unsubscribing from '/queue/testfile'") cli.onecmd("run %s" % f.name) teststdout.expect("Shutting down, please wait") cli.onecmd("quit") def testrunarg(self): f = create_test_file() teststdin = StubStdin() teststdout = StubStdout(self) teststdout.expect("CONNECTED") cli = StompCLI(host, port, username, password, "1.0", stdin=teststdin, stdout=teststdout) time.sleep(3) teststdout.expect("Subscribing to '/queue/testfile' with acknowledge set to 'auto', id set to '1'") teststdout.expect("this is a test") teststdout.expect("MESSAGE") teststdout.expect("Unsubscribing from '/queue/testfile'") cli.do_run(f.name) teststdout.expect("Shutting down, please wait") cli.onecmd("quit") def test_multicast(self): teststdin = StubStdin() teststdout = StubStdout(self) teststdout.expect("CONNECTED") cli = StompCLI(None, None, None, None, "multicast", stdin=teststdin, stdout=teststdout) teststdout.expect("Subscribing to '/queue/testmulticastclisubscribe' with acknowledge set to 'auto', id set to '1'") cli.onecmd("subscribe /queue/testmulticastclisubscribe") teststdout.expect("MESSAGE") teststdout.expect("this is a test") cli.onecmd("send /queue/testsubscribe this is a test") teststdout.expect("Shutting down, please wait") cli.onecmd("quit") # just here for coverage def test_on_disconnected_edgecase(self): teststdin = StubStdin() teststdout = StubStdout(self) teststdout.expect("CONNECTED") cli = StompCLI(host, port, username, password, "1.0", stdin=teststdin, stdout=teststdout) teststdout.expect(".*lost connection.*") cli.on_disconnected() stomp.py-8.1.0/tests/test_cli_ssl.py000066400000000000000000000017671440306521300175250ustar00rootroot00000000000000from stomp.__main__ import StompCLI from .testutils import * username = get_default_user() password = get_default_password() (sslhost, sslport) = get_ssl_host()[0] class TestSSLCLI(object): def test_ssl(self): teststdin = StubStdin() teststdout = StubStdout(self) teststdout.expect("CONNECTED") cli = StompCLI(sslhost, sslport, username, password, "1.0", use_ssl=True, stdin=teststdin, stdout=teststdout) time.sleep(3) teststdout.expect("Subscribing to '/queue/testsubscribe' with acknowledge set to 'auto', id set to '1'") cli.onecmd("subscribe /queue/testsubscribe") teststdout.expect("MESSAGE") teststdout.expect("this is a test") cli.onecmd("send /queue/testsubscribe this is a test") time.sleep(3) teststdout.expect("Unsubscribing from '/queue/testsubscribe'") cli.onecmd("unsubscribe /queue/testsubscribe") teststdout.expect("Shutting down, please wait") cli.onecmd("quit") stomp.py-8.1.0/tests/test_context.py000066400000000000000000000031361440306521300175510ustar00rootroot00000000000000import stomp from stomp.listener import TestListener from .testutils import * class TestContext(object): timestamp = time.strftime("%Y%m%d%H%M%S") def send_test_message(self, conn): listener = TestListener("123", print_to_log=True) conn.set_listener("testlistener", listener) conn.connect(get_default_user(), get_default_password(), wait=True) queuename = "/queue/test1-%s" % self.timestamp conn.subscribe(destination=queuename, id=1, ack="auto") conn.send(body="this is a test", destination=queuename, receipt="123") listener.wait_for_message() @staticmethod def after_test_message(conn): conn.remove_listener("testlistener") @staticmethod def check_asserts(conn): listener = conn.get_listener("testlistener") assert listener.connections == 1, "should have received 1 connection acknowledgement" assert listener.disconnects >= 1, "should have received 1 disconnect" def test_with_context_stomp11(self): with stomp.Connection11(get_default_host()) as conn: self.send_test_message(conn) self.check_asserts(conn) self.after_test_message(conn) def test_with_context_stomp10(self): with stomp.Connection10(get_default_host()) as conn: self.send_test_message(conn) self.check_asserts(conn) self.after_test_message(conn) def test_with_context_stomp12(self): with stomp.Connection12(get_default_host()) as conn: self.send_test_message(conn) self.check_asserts(conn) self.after_test_message(conn) stomp.py-8.1.0/tests/test_ipv6.py000066400000000000000000000015561440306521300167550ustar00rootroot00000000000000import stomp from stomp.listener import TestListener from stomp import logging from .testutils import * @pytest.fixture() def conn(): if not is_inside_travis(): conn = stomp.Connection11(get_ipv6_host()) conn.set_listener("testlistener", TestListener("123", print_to_log=True)) conn.connect("admin", "password", wait=True) yield conn conn.disconnect(receipt=None) else: yield None class TestIP6(object): def test_ipv6_send(self, conn): if not is_inside_travis(): logging.info("running ipv6 test") timestamp = time.strftime("%Y%m%d%H%M%S") queuename = "/queue/testipv6-%s" % timestamp conn.subscribe(destination=queuename, id=1, ack="auto") conn.send(body="this is a test", destination=queuename, receipt="123") validate_send(conn) stomp.py-8.1.0/tests/test_misc.py000066400000000000000000000142111440306521300170140ustar00rootroot00000000000000import importlib import platform import traceback import xml.dom.minidom import stomp from stomp.exception import * from stomp.listener import * from stomp import logging from .testutils import * class TransformationListener(TestListener): def __init__(self, receipt, print_to_log): TestListener.__init__(self, receipt, print_to_log) self.message = None def on_before_message(self, frame): if "transformation" in frame.headers: trans_type = frame.headers["transformation"] if trans_type != "jms-map-xml": return try: entries = {} doc = xml.dom.minidom.parseString(frame.body) root_elem = doc.documentElement for entryElem in root_elem.getElementsByTagName("entry"): pair = [] for node in entryElem.childNodes: if not isinstance(node, xml.dom.minidom.Element): continue pair.append(node.firstChild.nodeValue) assert len(pair) == 2 entries[pair[0]] = pair[1] frame.body = entries except Exception: # # unable to parse message. return original # traceback.print_exc() def on_message(self, frame): TestListener.on_message(self, frame) self.message = frame class HeaderModListener(TestListener): def on_before_message(self, frame): frame.headers["testheader"] = "modifiedheader" def on_message(self, frame): TestListener.on_message(self, frame) self.message = frame @pytest.fixture() def conn(): conn = stomp.Connection11(get_rabbitmq_host()) conn.set_listener("testlistener", TransformationListener("123", print_to_log=True)) conn.connect(get_rabbitmq_user(), get_rabbitmq_password(), wait=True) yield conn conn.disconnect(receipt=None) @pytest.fixture() def conn2(): conn2 = stomp.Connection11(get_rabbitmq_host()) conn2.set_listener("testlistener", HeaderModListener("123", print_to_log=True)) conn2.connect(get_rabbitmq_user(), get_rabbitmq_password(), wait=True) yield conn2 conn2.disconnect(receipt=None) def timeout_server(svr): time.sleep(3) logging.info("Stopping server %s" % svr) svr.running = False svr.stop() @pytest.fixture() def miscserver(): server = StubStompServer("127.0.0.1", 60000) server.start() yield server @pytest.fixture() def timeout_thread(miscserver): timeout_thread = threading.Thread(name="shutdown test server", target=timeout_server, args=(miscserver,)) yield timeout_thread class TestMessageTransform(object): def test_transform(self, conn): listener = conn.get_listener("testlistener") queuename = "/queue/testtransform-%s" % listener.timestamp conn.subscribe(destination=queuename, id=1, ack="auto") conn.send(body=''' name Dejan city Belgrade ''', destination=queuename, headers={"transformation": "jms-map-xml"}, receipt="123") listener.wait_on_receipt() listener.wait_for_message() message = listener.message.body assert message is not None, "Did not receive a message" assert type(message) == dict, "Message type should be dict after transformation, was %s" % message.__class__ assert message["name"] == "Dejan", "Missing an expected dict element" assert message["city"] == "Belgrade", "Missing an expected dict element" class TestNoResponseConnectionKill(object): def test_noresponse(self, miscserver, timeout_thread): try: conn = stomp.Connection([("127.0.0.1", 60000)], heartbeats=(1000, 1000)) listener = TestListener(print_to_log=True) conn.set_listener("testlistener", listener) timeout_thread.start() conn.connect(wait=True) pytest.fail("Shouldn't happen") except ConnectFailedException: logging.info("received connect failed - test success") except Exception as e: logging.error(e) pytest.fail("Shouldn't happen, error %s" % e) class TestMiscellaneousLogic(object): def test_windows_colours(self, mocker): platform.system = mocker.MagicMock(return_value="Windows") import stomp.colours importlib.reload(stomp.colours) assert "" == stomp.colours.GREEN assert "" == stomp.colours.RED assert "" == stomp.colours.BOLD assert "" == stomp.colours.NO_COLOUR # just here for coverage def test_publisher(self): p = Publisher() p.set_listener("testlistener", None) p.remove_listener("testlistener") assert p.get_listener("testlistener") is None # coverage improvement since on_heartbeat is handled in subclasses of ConnectionListener def test_on_heartbeat(self): cl = ConnectionListener() cl.on_heartbeat() def test_heartbeatlistener(self, mocker): transport = mocker.MagicMock() hl = HeartbeatListener(transport, (10000, 20000)) hl.on_connected(Frame('heartbeat', {"heart-beat": "10000,20000"}, '')) time.sleep(1) hl.on_message(Frame('')) # just check if there was a received heartbeat calculated assert hl.received_heartbeat > 0 # Shut down the heartbeat thread so it doesn't go into a busy loop hl.on_disconnected() def test_original_headers(self, conn2): listener = conn2.get_listener("testlistener") queuename = "/queue/testheadermod-%s" % listener.timestamp conn2.subscribe(destination=queuename, id=1, ack="auto") conn2.send(body="test message", destination=queuename, headers={"testheader": "originalheader"}, receipt="123") listener.wait_on_receipt() listener.wait_for_message() assert "modifiedheader" == listener.message.headers["testheader"] assert "originalheader" == listener.message.original_headers["testheader"] stomp.py-8.1.0/tests/test_multicast.py000066400000000000000000000061501440306521300200710ustar00rootroot00000000000000from stomp.adapter.multicast import MulticastConnection from stomp.listener import TestListener from .testutils import * @pytest.fixture def conn(): conn = MulticastConnection() listener = TestListener("123", print_to_log=True) conn.set_listener("testlistener", listener) conn.connect() yield conn conn.disconnect(receipt=None) @pytest.fixture def connutf16(): conn = MulticastConnection(encoding="utf-16") listener = TestListener("123", print_to_log=True) conn.set_listener("testlistener", listener) conn.connect() yield conn conn.disconnect(receipt=None) class TestMulticast(object): def testsubscribe(self, conn): listener = conn.get_listener("testlistener") queuename = "/queue/test1-%s" % listener.timestamp conn.subscribe(destination=queuename, id=1, ack="auto") conn.send(body="this is a test", destination=queuename, receipt="123") validate_send(conn, 1, 1, 0) def testunsubscribe(self, conn): listener = conn.get_listener("testlistener") queuename = "/queue/test1-%s" % listener.timestamp conn.subscribe(destination=queuename, id=1, ack="auto") conn.send(body="this is a test", destination=queuename, receipt="123") validate_send(conn, 1, 1, 0) conn.unsubscribe(1) conn.send(body="this is a test", destination=queuename, receipt="124") time.sleep(3) assert listener.messages == 1, "should have only received 1 message" def testtransactions(self, conn): listener = conn.get_listener("testlistener") queuename = "/queue/test1-%s" % listener.timestamp conn.subscribe(destination=queuename, id=1, ack="auto") trans_id = get_uuid() conn.begin(trans_id) conn.send(body="this is a test", transaction=trans_id, destination=queuename, receipt="123") time.sleep(1) assert listener.messages == 0, "should not have received any messages" conn.commit(trans_id) listener.wait_on_receipt() assert listener.messages == 1, "should have received 1 message" conn.begin(trans_id) conn.send(body="this is a test", transaction=trans_id, destination=queuename, receipt="124") conn.abort(trans_id) time.sleep(3) assert listener.messages == 1, "should have only received 1 message" class TestNonAsciiViaMulticast(object): def test_send_nonascii_auto_encoding(self, connutf16): listener = connutf16.get_listener("testlistener") queuename = "/queue/multicast-nonascii-%s" % listener.timestamp connutf16.subscribe(destination=queuename, ack="auto", id="1") txt = test_text_for_utf16 connutf16.send(body=txt, destination=queuename, receipt="123") listener.wait_for_message() assert listener.connections >= 1, "should have received 1 connection acknowledgement" assert listener.messages >= 1, "should have received 1 message" assert listener.errors == 0, "should not have received any errors" (_, msg) = listener.get_latest_message() assert txt.encode("utf-8") == msg stomp.py-8.1.0/tests/test_nonascii.py000066400000000000000000000123601440306521300176670ustar00rootroot00000000000000# -*- coding: UTF-8 -*- import filecmp import stomp from stomp.listener import * from .testutils import * @pytest.fixture def conn(): conn = stomp.Connection(get_default_host(), auto_decode=False) listener = TestListener("123", print_to_log=True) conn.set_listener("testlistener", listener) conn.connect(get_default_user(), get_default_password(), wait=True) yield conn conn.disconnect(receipt=None) @pytest.fixture def conn_encode(): conn = stomp.Connection(get_default_host(), auto_decode=True) listener = TestListener("123", print_to_log=True) conn.set_listener("testlistener", listener) conn.connect(get_default_user(), get_default_password(), wait=True) yield conn conn.disconnect(receipt=None) @pytest.fixture def conn_encode_utf18(): conn = stomp.Connection(get_default_host(), auto_decode=True, encoding="utf-16") listener = TestListener("123", print_to_log=True) conn.set_listener("testlistener", listener) conn.connect(get_default_user(), get_default_password(), wait=True) yield conn conn.disconnect(receipt=None) class TestNonAsciiSend(object): def test_send_nonascii(self, conn): listener = conn.get_listener("testlistener") queuename = "/queue/nonasciitest-%s" % listener.timestamp conn.subscribe(destination=queuename, ack="auto", id="1") txt = test_text_for_utf8 conn.send(body=txt, destination=queuename, receipt="123") listener.wait_for_message() assert listener.connections >= 1, "should have received 1 connection acknowledgement" assert listener.messages >= 1, "should have received 1 message" assert listener.errors == 0, "should not have received any errors" (_, msg) = listener.get_latest_message() assert encode(txt) == msg def test_image_send(self, conn): d = os.path.dirname(os.path.realpath(__file__)) srcname = os.path.join(d, "test.gif") with open(srcname, 'rb') as f: img = f.read() listener = conn.get_listener("testlistener") queuename = "/queue/nonascii-image-%s" % listener.timestamp conn.subscribe(destination=queuename, ack="auto", id="1") conn.send(body=img, destination=queuename, receipt="123") listener.wait_for_message() assert listener.connections >= 1, "should have received 1 connection acknowledgement" assert listener.messages >= 1, "should have received 1 message" assert listener.errors == 0, "should not have received any errors" (_, msg) = listener.get_latest_message() assert img == msg destname = os.path.join(d, "test-out.gif") with open(destname, 'wb') as f: f.write(img) assert filecmp.cmp(srcname, destname) def test_image_send(self, conn): d = os.path.dirname(os.path.realpath(__file__)) srcname = os.path.join(d, "test.gif.gz") with open(srcname, 'rb') as f: img = f.read() listener = conn.get_listener("testlistener") queuename = "/queue/nonascii-image-%s" % listener.timestamp conn.subscribe(destination=queuename, ack="auto", id="1") conn.send(body=img, destination=queuename, receipt="123") listener.wait_for_message() assert listener.connections >= 1, "should have received 1 connection acknowledgement" assert listener.messages >= 1, "should have received 1 message" assert listener.errors == 0, "should not have received any errors" (_, msg) = listener.get_latest_message() assert img == msg destname = os.path.join(d, "test-out.gif.gz") with open(destname, 'wb') as f: f.write(img) assert filecmp.cmp(srcname, destname) class TestNonAsciiSendAutoDecode(object): def test_send_nonascii_auto_decoding(self, conn_encode): listener = conn_encode.get_listener("testlistener") queuename = "/queue/nonasciitest2-%s" % listener.timestamp conn_encode.subscribe(destination=queuename, ack="auto", id="1") txt = test_text_for_utf8 conn_encode.send(body=txt, destination=queuename, receipt="123") listener.wait_for_message() assert listener.connections >= 1, "should have received 1 connection acknowledgement" assert listener.messages >= 1, "should have received 1 message" assert listener.errors == 0, "should not have received any errors" (_, msg) = listener.get_latest_message() assert txt == msg class TestNonAsciiSendSpecificEncoding(object): def test_send_nonascii_auto_encoding(self, conn_encode_utf18): listener = conn_encode_utf18.get_listener("testlistener") queuename = "/queue/nonasciitest2-%s" % listener.timestamp conn_encode_utf18.subscribe(destination=queuename, ack="auto", id="1") txt = test_text_for_utf16 conn_encode_utf18.send(body=txt, destination=queuename, receipt="123") listener.wait_for_message() assert listener.connections >= 1, "should have received 1 connection acknowledgement" assert listener.messages >= 1, "should have received 1 message" assert listener.errors == 0, "should not have received any errors" (_, msg) = listener.get_latest_message() assert txt == msg stomp.py-8.1.0/tests/test_override_threading.py000066400000000000000000000032011440306521300217220ustar00rootroot00000000000000from concurrent.futures import ThreadPoolExecutor import stomp from stomp.listener import TestListener from .testutils import * from stomp import logging executor = ThreadPoolExecutor() def create_thread(fc): f = executor.submit(fc) print("Created future %s on executor %s" % (f, executor)) return f class ReconnectListener(TestListener): def __init__(self, conn): TestListener.__init__(self, "123", True) self.conn = conn def on_receiver_loop_ended(self, *args): if self.conn: c = self.conn self.conn = None c.connect(get_default_user(), get_default_password(), wait=True) c.disconnect() @pytest.fixture def conn(): conn = stomp.Connection(get_default_host()) # check thread override here conn.transport.override_threading(create_thread) listener = ReconnectListener(conn) conn.set_listener("testlistener", listener) conn.connect(get_default_user(), get_default_password(), wait=True) yield conn class TestThreadingOverride(object): def test_threading(self, conn): listener = conn.get_listener("testlistener") queuename = "/queue/test1-%s" % listener.timestamp conn.subscribe(destination=queuename, id=1, ack="auto") conn.send(body="this is a test", destination=queuename, receipt="123") validate_send(conn, 1, 1, 0) logging.info("first disconnect") conn.disconnect(receipt="112233") logging.info("reconnecting") conn.connect(get_default_user(), get_default_password(), wait=True) logging.info("second disconnect") conn.disconnect() stomp.py-8.1.0/tests/test_rabbitmq.py000066400000000000000000000023551440306521300176700ustar00rootroot00000000000000import stomp from stomp.listener import TestListener, WaitingListener from .testutils import * @pytest.fixture() def conn(): conn = stomp.Connection11(get_rabbitmq_host()) listener = TestListener("123", print_to_log=True) listener2 = WaitingListener("456") conn.set_listener("123", listener) conn.set_listener("456", listener2) conn.connect(get_rabbitmq_user(), get_rabbitmq_password(), wait=True) yield conn class TestRabbitMQSend(object): def test_send(self, conn): listener = conn.get_listener("123") listener2 = conn.get_listener("456") queue_name = "/queue/test-%s" % listener.timestamp conn.subscribe(destination=queue_name, id=1, ack="auto") conn.send(body="this is a test", destination=queue_name, receipt="123") listener.wait_on_receipt() conn.disconnect(receipt="456") listener2.wait_on_receipt() assert listener.connections == 1, "should have received 1 connection acknowledgement" assert listener.messages == 1, "should have received 1 message" assert listener.errors == 0, "should not have received any errors" assert listener.disconnects == 1, "should have received 1 disconnect, was %s" % listener.disconnects stomp.py-8.1.0/tests/test_rabbitmq_ws.py000066400000000000000000000024451440306521300204010ustar00rootroot00000000000000import stomp from stomp.listener import TestListener, WaitingListener from .testutils import * import time @pytest.fixture() def conn(): conn = stomp.WSConnection(get_rabbitmq_ws_host(), vhost="/", ws_path="/ws", header={}) listener = TestListener("123", print_to_log=True) listener2 = WaitingListener("456") conn.set_listener("123", listener) conn.set_listener("456", listener2) conn.connect(get_rabbitmq_user(), get_rabbitmq_password(), wait=True) yield conn class TestRabbitMQSend(object): def test_send(self, conn): listener = conn.get_listener("123") listener2 = conn.get_listener("456") queue_name = "/queue/test-%s" % listener.timestamp conn.subscribe(destination=queue_name, id=1, ack="auto") conn.send(body="this is a test", destination=queue_name, receipt="123") listener.wait_on_receipt() conn.disconnect(receipt="456") listener2.wait_on_disconnected() assert listener.connections == 1, "should have received 1 connection acknowledgement" assert listener.messages == 1, "should have received 1 message" assert listener.errors == 0, "should not have received any errors" assert listener.disconnects == 1, "should have received 1 disconnect, was %s" % listener.disconnects stomp.py-8.1.0/tests/test_s10.py000066400000000000000000000023131440306521300164640ustar00rootroot00000000000000import stomp from stomp.listener import * from .testutils import * @pytest.fixture() def conn(): conn = stomp.Connection10(get_default_host()) listener = TestListener("123", print_to_log=True) conn.set_listener("testlistener", listener) conn.connect(get_default_user(), get_default_password(), wait=True) yield conn conn.disconnect(receipt=None) class Test10Connect(object): def testsend10(self, conn): listener = conn.get_listener("testlistener") queuename = "/queue/testsend10-%s" % listener.timestamp conn.subscribe(destination=queuename, ack="auto") conn.send(body="this is a test using protocol 1.0", destination=queuename, receipt="123") validate_send(conn, 1, 1, 0) def testclientack10(self, conn): listener = conn.get_listener("testlistener") queuename = "/queue/testclientack10-%s" % listener.timestamp conn.subscribe(destination=queuename, id=1, ack="client") conn.send(body="this is a test", destination=queuename) listener.wait_for_message() (headers, _) = listener.get_latest_message() message_id = headers["message-id"] conn.ack(message_id) time.sleep(1) stomp.py-8.1.0/tests/test_s11.py000066400000000000000000000074051440306521300164740ustar00rootroot00000000000000import stomp from stomp.listener import * from .testutils import * class Test11Send(object): def test11(self): conn = stomp.Connection(get_default_host()) tl = TestListener("123", print_to_log=True) conn.set_listener('', tl) conn.connect(get_default_user(), get_default_password(), wait=True) conn.subscribe(destination="/queue/test", ack="auto", id=1) conn.send(body="this is a test", destination="/queue/test", receipt="123") tl.wait_for_message() assert tl.connections == 1, "should have received 1 connection acknowledgement" assert tl.messages >= 1, "should have received at least 1 message" assert tl.errors == 0, "should not have received any errors" conn.unsubscribe(destination="/queue/test", id=1) wl = WaitingListener("DISCONNECT1") conn.set_listener("waiting", wl) # stomp1.1 disconnect with receipt conn.disconnect(receipt="DISCONNECT1") # wait for the receipt wl.wait_on_receipt() def testheartbeat(self): conn = stomp.Connection(get_default_host(), heartbeats=(2000, 3000)) listener = TestListener("123", print_to_log=True) conn.set_listener('', listener) conn.connect(get_default_user(), get_default_password(), wait=True) assert conn.heartbeats[0] > 0 conn.subscribe(destination="/queue/test", ack="auto", id=1) conn.send(body="this is a test", destination="/queue/test", receipt="123") listener.wait_for_message() conn.disconnect(receipt=None) assert listener.connections >= 1, "should have received 1 connection acknowledgement, was %s" % listener.connections assert listener.messages >= 1, "should have received 1 message, was %s" % listener.messages assert listener.errors == 0, "should not have received any errors, was %s" % listener.errors assert listener.heartbeat_timeouts == 0, "should not have received a heartbeat timeout, was %s" % listener.heartbeat_timeouts def testheartbeat_timeout(self): server = StubStompServer("127.0.0.1", 60000) server.start() try: server.add_frame('''CONNECTED version:1.1 session:1 server:test heart-beat:1000,1000 \x00''') conn = stomp.Connection([("127.0.0.1", 60000)], heartbeats=(1000, 1000)) listener = TestListener(print_to_log=True) conn.set_listener('', listener) conn.connect() time.sleep(5) server.running = False except Exception: _, e, _ = sys.exc_info() logging.error("error: %s", e) finally: server.stop() assert listener.heartbeat_timeouts >= 1, "should have received a heartbeat timeout" def testheartbeat_shutdown(self): server = StubStompServer("127.0.0.1", 60000) server.start() conn = None try: server.add_frame('''CONNECTED version:1.1 session:1 server:test heart-beat:1000,1000 \x00''') conn = stomp.Connection([("127.0.0.1", 60000)], heartbeats=(10000, 10000)) listener = TestListener(print_to_log=True) conn.set_listener('', listener) conn.connect() start_time = time.time() time.sleep(0.5) # shutdown connection server.stop() while conn.heartbeat_thread is not None: time.sleep(0.5) end_time = time.time() server.running = False except Exception: _, e, _ = sys.exc_info() logging.error("error: %s", e) assert end_time - start_time <= 2, "should stop immediately and not after heartbeat timeout" assert conn.heartbeat_thread is None, "heartbeat thread should have finished" stomp.py-8.1.0/tests/test_s12.py000066400000000000000000000113151440306521300164700ustar00rootroot00000000000000import stomp from stomp import exception from stomp.listener import TestListener from .testutils import * @pytest.fixture() def conn(): conn = stomp.Connection12(get_default_host()) listener = TestListener("123", print_to_log=True) conn.set_listener("testlistener", listener) conn.connect(get_default_user(), get_default_password(), wait=True) yield conn conn.disconnect(receipt=None) class Test12Connect(object): def test_send(self, conn): listener = conn.get_listener("testlistener") queuename = "/queue/testsend12-%s" % listener.timestamp conn.subscribe(destination=queuename, id=1, ack="auto") conn.send(body="this is a test using protocol 1.2", destination=queuename, receipt="123") validate_send(conn, 1, 1, 0) def test_clientack(self, conn): listener = conn.get_listener("testlistener") queuename = "/queue/testclientack12-%s" % listener.timestamp conn.subscribe(destination=queuename, id=1, ack="client-individual") conn.send(body="this is a test", destination=queuename, receipt="123") listener.wait_for_message() (headers, _) = listener.get_latest_message() ack_id = headers["ack"] conn.ack(ack_id) def test_clientnack(self, conn): listener = conn.get_listener("testlistener") queuename = "/queue/testclientnack12-%s" % listener.timestamp conn.subscribe(destination=queuename, id=1, ack="client-individual") conn.send(body="this is a test", destination=queuename, receipt="123") listener.wait_for_message() (headers, _) = listener.get_latest_message() ack_id = headers["ack"] conn.nack(ack_id) def test_should_send_extra_header_clientnack(self, conn, mocker): listener = conn.get_listener("testlistener") queuename = "/queue/testclientnack12-%s" % listener.timestamp conn.subscribe(destination=queuename, id=1, ack="client-individual") conn.send(body="this is a test", destination=queuename, receipt="123") listener.wait_for_message() (headers, _) = listener.get_latest_message() ack_id = headers["ack"] wrapped_send_frame = mocker.patch.object(conn, "send_frame", wraps=conn.send_frame) conn.nack(ack_id, requeue="false") expected_headers = {HDR_ID: ack_id.replace(':', '\\c'), "requeue": "false"} wrapped_send_frame.assert_called_with(CMD_NACK, expected_headers) def test_timeout(self): server = StubStompServer("127.0.0.1", 60000) try: server.start() server.add_frame('''ERROR message: connection failed\x00''') conn = stomp.Connection12([("127.0.0.1", 60000)]) listener = TestListener(print_to_log=True) conn.set_listener('', listener) try: conn.connect(wait=True) self.fail("shouldn't happen") except exception.ConnectFailedException: pass finally: server.stop() def test_specialchars(self, conn): listener = conn.get_listener("testlistener") queuename = "/queue/testspecialchars12-%s" % listener.timestamp conn.subscribe(destination=queuename, id=1, ack="client") hdrs = { "special-1": "test with colon : test", "special-2": "test with backslash \\ test", "special-3": "test with newlines \n \n", "special-4": "test with carriage return \r" } conn.send(body="this is a test", headers=hdrs, destination=queuename, receipt="123") listener.wait_for_message() (headers, _) = listener.get_latest_message() _ = headers["message-id"] _ = headers["subscription"] assert "special-1" in headers assert "test with colon : test" == headers["special-1"] assert "special-2" in headers assert "test with backslash \\ test" == headers["special-2"] assert "special-3" in headers assert "test with newlines \n \n" == headers["special-3"] assert "special-4" in headers cr_header = headers["special-4"].replace('\\r', '\r') assert "test with carriage return \r" == cr_header def test_suppress_content_length(self, conn, mocker): listener = conn.get_listener("testlistener") queuename = "/queue/testspecialchars12-%s" % listener.timestamp conn = stomp.Connection12(get_default_host(), vhost=get_default_vhost(), auto_content_length=False) conn.transport = mocker.Mock() conn.send(body="test", destination=queuename, receipt="123") args, kwargs = conn.transport.transmit.call_args frame = args[0] assert "content-length" not in frame.headers stomp.py-8.1.0/tests/test_ss.py000066400000000000000000000140661440306521300165160ustar00rootroot00000000000000import stomp from stomp.listener import * from .testutils import * @pytest.fixture def server(): server = StubStompServer("127.0.0.1", 60000) server.start() yield server server.stop() class TestWithStompServer(object): def test_alternate_hosts(self, server): server.add_frame('''CONNECTED version:1.1 session:1 server:test heart-beat:1000,1000 \x00''') stomp.logging.verbose = True conn = stomp.Connection([("192.0.2.0", 10000), ("127.0.0.1", 60000)], timeout=1, prefer_localhost=False) listener = TestListener(print_to_log=True) conn.set_listener('', listener) conn.connect(wait=True) def test_disconnect(self, server): server.add_frame('''CONNECTED version:1.1 session:1 server:test heart-beat:1000,1000 \x00''') conn = stomp.Connection([("127.0.0.1", 60000)]) listener = TestListener(print_to_log=True) conn.set_listener('', listener) conn.connect() time.sleep(2) server.stop() for _ in range(100): if server.stopped: break time.sleep(0.1) else: assert False, "server never disconnected" time.sleep(1) try: conn.send(body="test disconnect", destination="/test/disconnectqueue") pytest.fail("Should not have successfully sent a message at this point") except Exception: _, e, _ = sys.exc_info() if e.__class__ == AssertionError: pytest.fail(str(e)) logging.debug("stopping conn after expected exception %s", e) # lost connection, now restart the server try: conn.disconnect(receipt=None) except: pass time.sleep(2) server.add_frame('''CONNECTED version:1.1 session:1 server:test heart-beat:1000,1000 \x00''') server.start() conn.connect() time.sleep(5) assert listener.connections >= 2, "should have received 2 connection acknowledgements" def test_parsing(self, server): def pump(n): # pump; test server gives us one frame per received something for x in range(n): if x == 0: logging.debug("pump sending %s frames" % n) conn.transport.send(b"\n") time.sleep(0.01) # Trailing optional EOLs in a frame server.add_frame('''CONNECTED version:1.1 session:1 server:test heart-beat:1000,1000 \x00''' + "\n\n\n") expected_heartbeat_count = 0 conn = stomp.Connection([("127.0.0.1", 60000)], heartbeats=(1000, 1000)) listener = TestListener(print_to_log=True) conn.set_listener('', listener) conn.connect() logging.info("test parsing (1) expected hb count is %s", expected_heartbeat_count) assert expected_heartbeat_count == listener.heartbeat_count, "(1) expected hb count %s, was %s" % (expected_heartbeat_count, listener.heartbeat_count) # No trailing EOLs, separate heartbeat message_body = "Hello\n...world!" message_frame = '''MESSAGE content-type:text/plain %s\x00''' % message_body server.add_frame(message_frame) server.add_frame("\n") expected_heartbeat_count += 1 pump(2) logging.info("test parsing (2) expected hb count is %s", expected_heartbeat_count) listener.wait_for_heartbeat() headers, body = listener.get_latest_message() assert expected_heartbeat_count == listener.heartbeat_count, "(2) expected hb count %s, was %s" % (expected_heartbeat_count, listener.heartbeat_count) assert {"content-type": "text/plain"} == headers assert message_body == body # Trailing EOL, separate heartbeat, another message server.add_frame(message_frame + "\n") server.add_frame("\n") server.add_frame(message_frame + "\n") expected_heartbeat_count += 1 pump(3) logging.info("test parsing (3) expected hb count is %s", expected_heartbeat_count) listener.wait_for_heartbeat() listener.wait_for_message() headers, body = listener.get_latest_message() assert expected_heartbeat_count == listener.heartbeat_count, "(3) expected hb count %s, was %s" % (expected_heartbeat_count, listener.heartbeat_count) assert {"content-type": "text/plain"} == headers assert message_body == body # Torture tests: return content one byte at a time server.add_frame("\n") server.add_frame(message_frame) server.add_frame("\n") expected_heartbeat_count += 2 pump(len(message_frame) + 2) logging.info("test parsing (4) expected hb count is %s", expected_heartbeat_count) listener.wait_for_heartbeat() headers, body = listener.get_latest_message() assert expected_heartbeat_count == listener.heartbeat_count, "(4) expected hb count %s, was %s" % (expected_heartbeat_count, listener.heartbeat_count) assert {"content-type": "text/plain"} == headers assert message_body == body # ...and a similar one with content-length and null bytes in body message_body = "%s\x00\x00%s" % (message_body, message_body) message_frame = '''MESSAGE content-type:text/plain content-length:%s %s\x00''' % (len(message_body), message_body) server.add_frame("\n") server.add_frame("\n") server.add_frame(message_frame) server.add_frame("\n") expected_heartbeat_count += 3 pump(len(message_frame) + 3) logging.info("test parsing (5) expected hb count is %s", expected_heartbeat_count) listener.wait_for_heartbeat() headers, body = listener.get_latest_message() assert expected_heartbeat_count == listener.heartbeat_count, "(5) expected hb count %s, was %s" % (expected_heartbeat_count, listener.heartbeat_count) assert { "content-type": "text/plain", "content-length": str(len(message_body)), } == headers assert message_body == body stomp.py-8.1.0/tests/test_ssl.py000066400000000000000000000156161440306521300166740ustar00rootroot00000000000000import importlib import pytest import stomp from stomp import transport from stomp.listener import TestListener from stomp import logging from .testutils import * host1 = get_ssl_host()[0] host2 = get_ssl_host()[0] ssl_key_file = "ssl_key_file" ssl_cert_file = "ssl_cert_file" ssl_ca_certs = "ssl_ca_certs" ssl_cert_validator = "ssl_cert_validator" ssl_version = "version" # Necessary to see the root cause of SSL errors logging.log_to_stdout(verbose_logging=True) @pytest.fixture def stomp_transport(): t = transport.Transport(host_and_ports=[host1, host2]) yield t class TestSSL(object): def test_ssl_connection(self): listener = TestListener("123", print_to_log=True) try: import ssl queuename = "/queue/testssl-%s" % listener.timestamp conn = stomp.Connection(get_ssl_host()) #conn.set_ssl(get_ssl_host()) conn.set_ssl(get_ssl_host()) conn.set_listener("testlistener", listener) conn.connect(get_default_user(), get_default_password(), wait=True) conn.subscribe(destination=queuename, id=1, ack="auto") conn.send(body="this is a test", destination=queuename, receipt="123") listener.wait_on_receipt() listener.wait_for_message() conn.disconnect(receipt=None) assert conn.get_ssl() is not None assert listener.connections == 1, "should have received 1 connection acknowledgement" assert listener.messages == 1, "should have received 1 message" assert listener.errors == 0, "should not have received any errors" except ImportError: pass def test_ssl_client_cert_connection(self): listener = TestListener("123", print_to_log=True) try: import ssl queuename = "/queue/testsslclient-%s" % listener.timestamp conn = stomp.Connection(get_ssl_host()) conn.set_ssl(get_ssl_host(), key_file='tmp/client.key', cert_file='tmp/client.pem', ca_certs='tmp/broker.pem') conn.set_listener("testlistener", listener) conn.connect(get_default_user(), get_default_password(), wait=True) conn.subscribe(destination=queuename, id=1, ack="auto") conn.send(body="this is a test with client cert", destination=queuename, receipt="123") listener.wait_on_receipt() listener.wait_for_message() conn.disconnect(receipt=None) assert conn.get_ssl() is not None assert listener.connections == 1, "should have received 1 connection acknowledgement" assert listener.messages == 1, "should have received 1 message" assert listener.errors == 0, "should not have received any errors" except ImportError: pass def test_ssl_invalid_broker_cert_connection(self): listener = TestListener("123", print_to_log=True) try: import ssl queuename = "/queue/testsslclient-%s" % listener.timestamp conn = stomp.Connection(get_ssl_host()) conn.set_ssl(get_ssl_host(), key_file='tmp/client.key', cert_file='tmp/client.pem', ca_certs='tmp/broker2.pem') conn.set_listener("testlistener", listener) try: conn.connect(get_default_user(), get_default_password(), wait=True) self.fail("Shouldn't get a successful connection") except: pass assert listener.connections == 0, "should not have received any connection acknowledgements" assert listener.messages == 0, "should not have received any messages" assert listener.errors == 0, "should not have received any errors" except ImportError: pass def test_ssl_expired_broker_cert_connection(self): listener = TestListener("123", print_to_log=True) try: import ssl queuename = "/queue/testsslexpired-%s" % listener.timestamp conn = stomp.Connection(get_expired_ssl_host()) conn.set_ssl(get_expired_ssl_host(), ca_certs='tmp/expiredbroker.pem') conn.set_listener("testlistener", listener) try: conn.connect(get_default_user(), get_default_password(), wait=True) pytest.fail("Shouldn't get a successful connection") except: pass assert listener.connections == 0, "should not have received any connection acknowledgements" assert listener.messages == 0, "should not have received any messages" assert listener.errors == 0, "should not have received any errors" except ImportError: pass class TestSSLParams(object): def test_set_ssl(self, stomp_transport): stomp_transport.set_ssl([host1], ssl_key_file, ssl_cert_file, ssl_ca_certs, ssl_cert_validator, ssl_version) assert stomp_transport._Transport__ssl_params[host1]["key_file"] == ssl_key_file assert stomp_transport._Transport__ssl_params[host1]["cert_file"] == ssl_cert_file assert stomp_transport._Transport__ssl_params[host1]["ca_certs"] == ssl_ca_certs assert stomp_transport._Transport__ssl_params[host1]["cert_validator"] == ssl_cert_validator assert stomp_transport._Transport__ssl_params[host1]["ssl_version"] == ssl_version def test_init_ssl_params(self): trans = transport.Transport(host_and_ports=[host1, host2]) trans.set_ssl([host1, host2], ssl_key_file, ssl_cert_file, ssl_ca_certs, ssl_cert_validator, ssl_version) for host_port in [host1, host2]: assert trans._Transport__ssl_params[host_port]["key_file"] == ssl_key_file assert trans._Transport__ssl_params[host_port]["cert_file"] == ssl_cert_file assert trans._Transport__ssl_params[host_port]["ca_certs"] == ssl_ca_certs assert trans._Transport__ssl_params[host_port]["cert_validator"] == ssl_cert_validator assert trans._Transport__ssl_params[host_port]["ssl_version"] == ssl_version class TestSSLFailure(object): @pytest.mark.run(order=-1) def test_ssl_failure(self, monkeypatch): import ssl monkeypatch.delattr(ssl, "PROTOCOL_TLS_CLIENT", raising=True) import stomp.transport as t importlib.reload(t) assert t.DEFAULT_SSL_VERSION is None monkeypatch.undo() importlib.reload(ssl) @pytest.mark.run(order=-1) def test_socket_failure(self, monkeypatch): import socket monkeypatch.delattr(socket, "SOL_TCP", raising=True) monkeypatch.delattr(socket, "SO_KEEPALIVE", raising=True) import stomp.transport as t importlib.reload(t) assert not t.LINUX_KEEPALIVE_AVAIL monkeypatch.undo() importlib.reload(socket) stomp.py-8.1.0/tests/test_ssl_sni.py000066400000000000000000000031731440306521300175400ustar00rootroot00000000000000import socket import stomp from stomp.listener import TestListener from .testutils import * class TestSNIMQSend(object): """ To test SNI: - Start the docker container Connections with SNI to "my.example.com" will be routed to the STOMP server on port 62613. Connections without SNI won't be routed. """ def testconnect(self, monkeypatch): def getaddrinfo_fake(host, port, *args, **kw): """Always return the IP address of the container.""" return [(socket.AF_INET, socket.SOCK_STREAM, socket.IPPROTO_TCP, '', ('172.17.0.2', port))] monkeypatch.setattr(socket, "getaddrinfo", getaddrinfo_fake) if not is_inside_travis(): logging.info("running ipv6 test") receipt_id = str(uuid.uuid4()) conn = stomp.Connection11(get_sni_ssl_host()) conn.set_ssl(get_sni_ssl_host()) listener = TestListener(receipt_id, print_to_log=True) conn.set_listener('', listener) conn.connect(get_default_user(), get_default_password(), wait=True) conn.subscribe(destination="/queue/test", id=1, ack="auto") logging.info("sending message with receipt %s" % receipt_id) conn.send(body="this is a test", destination="/queue/test", receipt=receipt_id) listener.wait_for_message() conn.disconnect(receipt=None) assert listener.connections == 1, "should have received 1 connection acknowledgement" assert listener.messages >= 1, "should have received 1 message" assert listener.errors == 0, "should not have received any errors" stomp.py-8.1.0/tests/test_stompserver.py000066400000000000000000000010641440306521300204540ustar00rootroot00000000000000import stomp from stomp.listener import TestListener from .testutils import * @pytest.fixture() def conn(): conn = stomp.Connection10(get_stompserver_host()) conn.set_listener("testlistener", TestListener("123", print_to_log=True)) conn.connect(wait=True) yield conn conn.disconnect(receipt=None) class TestStompServer(object): def test_send(self, conn): conn.subscribe(destination="/queue/test", id=1, ack="auto") conn.send(body="this is a test", destination="/queue/test", receipt="123") validate_send(conn) stomp.py-8.1.0/tests/test_threading.py000066400000000000000000000076331440306521300200400ustar00rootroot00000000000000"""Test that mq sends don't wedge their threads. Starts a number of sender threads, and runs for a set amount of time. Each thread sends messages as fast as it can, and after each send, pops from a Queue. Meanwhile, the Queue is filled with one marker per second. If the Queue fills, the test fails, as that indicates that all threads are no longer emptying the queue, and thus must be wedged in their send() calls. """ from queue import Empty, Full, Queue from time import monotonic import stomp from .testutils import * global mq mq = None class MQ(object): def __init__(self): self.conn = stomp.Connection(get_default_host()) self.conn.set_listener('', None) self.conn.connect("admin", "password", wait=True) def send(self, topic, msg, persistent="true", retry=False): self.conn.send(destination="/topic/%s" % topic, body=msg, persistent=persistent) def get_mq(): global mq if mq is None: mq = MQ() return mq class TestThreading(object): def init(self): self.mq = get_mq() self.q = Queue(10) self.cmd = Queue() self.error = Queue() self.clients = 20 self.threads = [] self.runfor = 20 for i in range(0, self.clients): t = threading.Thread(name="client %s" % i, target=self.make_sender(i)) t.daemon = True self.threads.append(t) def shutdown(self): for t in self.threads: if not t.is_alive(): print("thread", t, "died") self.cmd.put("stop") for t in self.threads: t.join() print() print() errs = [] while 1: try: errs.append(self.error.get(block=False)) except Empty: break print("Dead threads:", len(errs), "of", self.clients) etype = {} for ec, _, _ in errs: if ec in etype: etype[ec] += 1 else: etype[ec] = 1 for k in sorted(etype.keys()): print("%s: %s" % (k, etype[k])) get_mq().conn.disconnect() def make_sender(self, i): q = self.q cmd = self.cmd error = self.error def send(i=i, q=q, cmd=cmd, error=error): counter = 0 print("%s starting" % i) try: while 1: # print "%s sending %s" % (i, counter) try: get_mq().send("testclientwedge", "Message %s:%s" % (i, counter)) except: error.put(sys.exc_info()) # thread will die raise else: # print "%s sent %s" % (i, counter) try: q.get(block=False) except Empty: pass try: if cmd.get(block=False): break except Empty: pass counter += 1 finally: print("final", i, counter) return send def test_threads_dont_wedge(self): if os.environ.get("TEST_THREADS_DONT_WEDGE", "false") == "true": self.init() for t in self.threads: t.start() start = monotonic() while monotonic() - start < self.runfor: try: self.q.put(1, False) time.sleep(1.0) except Full: logging.info("passed") assert False, "Failed: 'request' queue filled up" self.shutdown() else: logging.info("TEST_THREADS_DONT_WEDGE property is not set - not running threading test") stomp.py-8.1.0/tests/test_transport.py000066400000000000000000000025571440306521300201270ustar00rootroot00000000000000import pytest import stomp from stomp import logging @pytest.fixture def stomp_transport(): transport = stomp.transport.BaseTransport() yield transport class TestTransport(object): def test_should_reject_null_listener(self, stomp_transport): with pytest.raises(AssertionError): stomp_transport.set_listener("testlistener", None) def test_process_frame_unknown_command_empty_body(self, stomp_transport): fr = stomp.utils.Frame("test", {}, None) stomp_transport.process_frame(fr, None) def test_process_frame_empty_body(self, stomp_transport): logging.setLevel(logging.INFO) fr = stomp.utils.Frame("error", {}, None) stomp_transport.process_frame(fr, None) def test_process_frame_unknown_command(self, stomp_transport): fr = stomp.utils.Frame("test", {}, "test message") stomp_transport.process_frame(fr, None) def test_process_frame(self, stomp_transport): logging.setLevel(logging.INFO) fr = stomp.utils.Frame("error", {}, "test message") stomp_transport.process_frame(fr, None) # just for coverage def test_methods_for_coverage(self, stomp_transport): stomp_transport.send(None) stomp_transport.receive() stomp_transport.cleanup() stomp_transport.attempt_connection() stomp_transport.disconnect_socket() stomp.py-8.1.0/tests/test_utils.py000066400000000000000000000100061440306521300172170ustar00rootroot00000000000000import importlib import pytest import stomp from stomp.utils import * class TestUtils(object): def test_returns_true_when_localhost(self): assert 1 == is_localhost(("localhost", 8000)) assert 1 == is_localhost(("127.0.0.1", 8000)) assert 2 == is_localhost(("192.168.1.92", 8000)) def test_convert_frame(self): f = Frame("SEND", { "header1": "value1", "headerNone": None, " no ": " trimming ", }, "this is the body") lines = convert_frame(f) s = pack(lines) assert bytearray("SEND\n no : trimming \nheader1:value1\n\nthis is the body\x00", "ascii") == s def test_parse_headers(self): lines = [ r"h1:foo\c\\bar ", r"h1:2nd h1 ignored -- not a must, but allowed and that is how we behave ATM", r"h\c2:baz\r\nquux", r"h3:\\n\\c", r"against-spec:\t", # should actually raise or something, we're against spec here ATM r" foo : bar", ] assert { "h1": r"foo:\bar ", "h:2": "baz\r\nquux", "h3": r"\n\c", "against-spec": r"\t", " foo ": " bar", } == parse_headers(lines) def test_calculate_heartbeats(self): chb = (3000, 5000) shb = map(str, reversed(chb)) assert (3000, 5000) == calculate_heartbeats(shb, chb) shb = ("6000", "2000") assert (3000, 6000) == calculate_heartbeats(shb, chb) shb = ("0", "0") assert (0, 0) == calculate_heartbeats(shb, chb) shb = ("10000", "0") assert (0, 10000) == calculate_heartbeats(shb, chb) chb = (0, 0) assert (0, 0) == calculate_heartbeats(shb, chb) def test_parse_frame(self): # oddball/broken f = parse_frame(b"FOO") assert str(f) == str(Frame("FOO", body=b'')) # empty body f = parse_frame(b"RECEIPT\nreceipt-id:message-12345\n\n") assert str(f) == str(Frame("RECEIPT", {"receipt-id": "message-12345"}, b'')) # no headers f = parse_frame(b"ERROR\n\n") assert str(f) == str(Frame("ERROR", body=b'')) # regular, different linefeeds for lf in b"\n", b"\r\n": f = parse_frame( b"MESSAGE" + lf + b"content-type:text/plain" + lf + lf + b"hello world!" ) assert str(f) == str(Frame("MESSAGE", {"content-type": "text/plain"}, b"hello world!")) def test_clean_default_headers(self): Frame('test').headers["foo"] = "bar" assert Frame('test').headers == {} def test_join(self): str = stomp.utils.join((b'a', b'b', b'c')) assert "abc" == str def test_decode(self): assert decode(None) is None assert "test" == decode(b"test") def test_encode(self): assert b"test" == encode("test") assert b"test" == encode(b"test") with pytest.raises(TypeError): encode(None) def test_pack(self): assert b"testtest" == pack([b"test", b"test"]) def test_should_skip_hostname_scan(self): os.environ["STOMP_SKIP_HOSTNAME_SCAN"] = "true" importlib.reload(stomp.utils) assert 2 == len(stomp.utils.LOCALHOST_NAMES) assert "localhost" in stomp.utils.LOCALHOST_NAMES assert "127.0.0.1" in stomp.utils.LOCALHOST_NAMES del os.environ["STOMP_SKIP_HOSTNAME_SCAN"] importlib.reload(stomp.utils) assert len(stomp.utils.LOCALHOST_NAMES) > 2 def test_mask_passcodes(self): lines = [ "test line 1", "test line with passcode: somepassword", "test line 3" ] lines = stomp.utils.clean_lines(lines) assert """['test line 1', 'test line with passcode:********', 'test line 3']""" == lines # just for coverage def test_get_errno(self): class ErrObj(object): pass o = ErrObj() o.args = ["a"] assert "a" == stomp.utils.get_errno(o) stomp.py-8.1.0/tests/testutils.py000066400000000000000000000172011440306521300170640ustar00rootroot00000000000000# -*- coding: utf8 -*- from configparser import RawConfigParser import json import os import sys import time from subprocess import run, PIPE import pytest from stomp.utils import * from stomp import logging config = RawConfigParser() config.read(os.path.join(os.path.dirname(__file__), "setup.ini")) header_re = re.compile(r"[^:]+:.*") test_text_for_utf8 = "марко" test_text_for_utf16 = "ǰ捁楴敶免" def get_environ(name): try: return os.environ[name] except: return None def get_default_host(): host = config.get("default", "host") port = config.get("default", "port") return [(get_environ("STD_HOST") or host, int(get_environ("STD_PORT") or port))] def get_default_vhost(): try: vhost = config.get("default", "vhost") except: vhost = None return get_environ("STD_VHOST") or vhost def get_default_user(): user = config.get("default", "user") return get_environ("STD_USER") or user def get_default_password(): password = config.get("default", "password") return get_environ("STD_PASSWORD") or password def get_ipv6_host(): if config.has_option("ipv6", "host"): host = config.get("ipv6", "host") elif os.environ.get('CONTAINERS_RUNROOT'): # Running under "podman unshare" result = run(["podman", "inspect", "stomppy", "-f", "{{.NetworkSettings.Networks.stomptest.GlobalIPv6Address}}"], stdout=PIPE) host = result.stdout.decode("utf-8").strip() else: result = run(["docker", "ps", "-f", "name=stomppy", "--format", "{{.ID}}"], stdout=PIPE) container_id = result.stdout.decode("utf-8").rstrip() result = run(["docker", "inspect", container_id], stdout=PIPE) j = json.loads(result.stdout.decode("utf-8")) j = j[0] network = j["NetworkSettings"] host = network["GlobalIPv6Address"] port = config.get("ipv6", "port") return [(get_environ("IPV6_HOST") or host, int(get_environ("IPV6_PORT") or port))] def get_ssl_host(): host = config.get("default", "host") port = config.get("default", "ssl_port") return [(get_environ("STD_HOST") or host, int(get_environ("STD_SSL_PORT") or port))] def get_expired_ssl_host(): host = config.get("default", "host") port = config.get("default", "ssl_expired_port") return [(get_environ("STD_HOST") or host, int(get_environ("STD_SSL_EXPIRED_PORT") or port))] def get_sni_ssl_host(): host = config.get("sni", "host") port = config.get("sni", "ssl_port") return [(get_environ("SNI_HOST") or host, int(get_environ("SNI_SSL_PORT") or port))] def get_rabbitmq_host(): host = config.get("rabbitmq", "host") port = config.get("rabbitmq", "port") return [(get_environ("RABBITMQ_HOST") or host, int(get_environ("RABBITMQ_PORT") or port))] def get_rabbitmq_ws_host(): host = config.get("rabbitmq_ws", "host") port = config.get("rabbitmq_ws", "port") return [(get_environ("RABBITMQ_WS_HOST") or host, int(get_environ("RABBITMQ_WS_PORT") or port))] def get_rabbitmq_user(): user = config.get("rabbitmq", "user") return get_environ("RABBITMQ_USER") or user def get_rabbitmq_password(): password = config.get("rabbitmq", "password") return get_environ("RABBITMQ_PASSWORD") or password def get_stompserver_host(): host = config.get("stompserver", "host") port = config.get("stompserver", "port") return [(get_environ("STOMPSERVER_HOST") or host, int(get_environ("STOMPSERVER_PORT") or port))] def get_artemis_host(): host = config.get("artemis", "host") port = config.get("artemis", "port") return [(get_environ("ARTEMIS_HOST") or host, int(get_environ("ARTEMIS_PORT") or port))] def get_artemis_user(): user = config.get("artemis", "user") return get_environ("ARTEMIS_USER") or user def get_artemis_password(): password = config.get("artemis", "password") return get_environ("ARTEMIS_PASSWORD") or password class StubStompServer(object): def __init__(self, host, port): self.host = host self.port = port self.frames = [] def start(self): logging.info("starting stomp server") self.s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) self.s.bind((self.host, self.port)) self.s.listen(1) self.running = True thread = threading.Thread(None, self.run) thread.daemon = True thread.start() self.stopped = False logging.info("stomp server started") def stop(self): logging.info("stopping test server") if self.conn: try: self.conn.shutdown(socket.SHUT_WR) except Exception: pass self.conn.close() if self.s: self.s.close() self.running = False self.conn = None self.s = None self.stopped = True logging.info("connection stopped") def get_next_frame(self): if len(self.frames) > 0: rtn = self.frames[0] del self.frames[0] return rtn else: return None def add_frame(self, frame): self.frames.append(frame) def run(self): self.conn, _ = self.s.accept() while self.running: try: _ = self.conn.recv(1024) frame = self.get_next_frame() if self.conn is None: break if frame is not None: logging.info("stompserver sending frame %s", frame) self.conn.send(encode(frame)) except Exception: _, e, _ = sys.exc_info() logging.debug(e) break time.sleep(0.1) try: self.conn.close() except: pass self.stopped = True logging.info("run loop completed") class StubStdin(object): pass class StubStdout(object): def __init__(self, test): self.expects = [] def expect(self, txt): self.expects.insert(0, re.compile(txt)) def write(self, txt): txt = txt.rstrip() if txt != '': print(txt) if txt == '>' or txt == '' or header_re.match(txt): return if len(self.expects) == 0: pytest.fail("No expectations - actual '%s'" % txt) return for x in range(0, len(self.expects)): chk = self.expects[x] if chk.match(txt): del self.expects[x] return pytest.fail("'%s' was not expected (expectations were: [%s])" % (txt, self.expects)) def flush(self): pass def validate_send(conn, connections=1, messages=1, errors=0): listener = conn.get_listener("testlistener") listener.wait_on_receipt() listener.wait_for_message() assert listener.connections == 1, "should have received 1 connection acknowledgement" assert listener.messages == 1, "should have received 1 message" assert listener.errors == 0, "should not have received any errors" def is_inside_travis(): if os.environ.get("TRAVIS", "false") == "true": logging.info("not running test inside travis") return True return False # snaffled from stackoverflow: https://codereview.stackexchange.com/questions/216037/python-scanner-for-the-first-free-port-in-a-range def next_free_port(port=1024, max_port=65535): sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) while port <= max_port: try: sock.bind(("", port)) sock.close() return port except OSError: port += 1 raise IOError("no free ports")