diff --git a/Makefile b/Makefile index c8524b1c..7db8f49d 100644 --- a/Makefile +++ b/Makefile @@ -5,7 +5,7 @@ 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.22.0 +ARTEMIS_VERSION=2.23.1 TEST_CMD := $(shell podman network exists stomptest &> /dev/null && echo "podman unshare --rootless-netns poetry" || echo "poetry") all: test install @@ -48,7 +48,7 @@ release: updateversion 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 docker/tmp/activemq-artemis-bin.tar.gz + wget http://www.apache.org/dist/activemq/activemq-artemis/${ARTEMIS_VERSION}/apache-artemis-${ARTEMIS_VERSION}-bin.tar.gz -O $@ || rm $@ ssl-setup: @@ -77,7 +77,7 @@ docker-image: docker/tmp/activemq-artemis-bin.tar.gz ssl-setup 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 --name stomppy -it stomppy + 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" @@ -100,7 +100,7 @@ podman-image: docker/tmp/activemq-artemis-bin.tar.gz ssl-setup 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 --name stomppy -it stomppy + 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" diff --git a/docker/Dockerfile b/docker/Dockerfile index df8d81d7..5fe30014 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -19,6 +19,7 @@ RUN apt update && apt install -y \ # 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 @@ -63,5 +64,7 @@ EXPOSE 62614/tcp EXPOSE 62619/tcp EXPOSE 63613/tcp EXPOSE 64613/tcp +EXPOSE 15674/tcp + ENTRYPOINT /bin/bash diff --git a/poetry.lock b/poetry.lock index bb421da4..9303fce4 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,6 +1,6 @@ [[package]] name = "atomicwrites" -version = "1.4.0" +version = "1.4.1" description = "Atomic file writes." category = "dev" optional = false @@ -22,7 +22,7 @@ tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (> [[package]] name = "colorama" -version = "0.4.4" +version = "0.4.5" description = "Cross-platform colored terminal text." category = "dev" optional = false @@ -98,8 +98,8 @@ python-versions = ">=3.6" importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} [package.extras] -dev = ["pre-commit", "tox"] -testing = ["pytest", "pytest-benchmark"] +testing = ["pytest-benchmark", "pytest"] +dev = ["tox", "pre-commit"] [[package]] name = "py" @@ -155,7 +155,7 @@ coverage = {version = ">=5.2.1", extras = ["toml"]} pytest = ">=4.6" [package.extras] -testing = ["fields", "hunter", "process-tests", "six", "pytest-xdist", "virtualenv"] +testing = ["virtualenv", "pytest-xdist", "six", "process-tests", "hunter", "fields"] [[package]] name = "pytest-html" @@ -221,6 +221,19 @@ category = "dev" optional = false python-versions = ">=3.6" +[[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" @@ -236,131 +249,27 @@ testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytes [metadata] lock-version = "1.1" python-versions = "^3.6" -content-hash = "60d85e2520a7f5230659a27aec58eef734bdc36da0982a29f9ca3eccfc83d8e1" +content-hash = "48ffba54b47d04f160b0572c4bf1192209eb64b6c4361dd94536698b51fd313e" [metadata.files] -atomicwrites = [ - {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"}, - {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"}, -] -attrs = [ - {file = "attrs-21.4.0-py2.py3-none-any.whl", hash = "sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4"}, - {file = "attrs-21.4.0.tar.gz", hash = "sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd"}, -] -colorama = [ - {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, - {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, -] -coverage = [ - {file = "coverage-6.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6dbc1536e105adda7a6312c778f15aaabe583b0e9a0b0a324990334fd458c94b"}, - {file = "coverage-6.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:174cf9b4bef0db2e8244f82059a5a72bd47e1d40e71c68ab055425172b16b7d0"}, - {file = "coverage-6.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:92b8c845527eae547a2a6617d336adc56394050c3ed8a6918683646328fbb6da"}, - {file = "coverage-6.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:c7912d1526299cb04c88288e148c6c87c0df600eca76efd99d84396cfe00ef1d"}, - {file = "coverage-6.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d5d2033d5db1d58ae2d62f095e1aefb6988af65b4b12cb8987af409587cc0739"}, - {file = "coverage-6.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:3feac4084291642165c3a0d9eaebedf19ffa505016c4d3db15bfe235718d4971"}, - {file = "coverage-6.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:276651978c94a8c5672ea60a2656e95a3cce2a3f31e9fb2d5ebd4c215d095840"}, - {file = "coverage-6.2-cp310-cp310-win32.whl", hash = "sha256:f506af4f27def639ba45789fa6fde45f9a217da0be05f8910458e4557eed020c"}, - {file = "coverage-6.2-cp310-cp310-win_amd64.whl", hash = "sha256:3f7c17209eef285c86f819ff04a6d4cbee9b33ef05cbcaae4c0b4e8e06b3ec8f"}, - {file = "coverage-6.2-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:13362889b2d46e8d9f97c421539c97c963e34031ab0cb89e8ca83a10cc71ac76"}, - {file = "coverage-6.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:22e60a3ca5acba37d1d4a2ee66e051f5b0e1b9ac950b5b0cf4aa5366eda41d47"}, - {file = "coverage-6.2-cp311-cp311-win_amd64.whl", hash = "sha256:b637c57fdb8be84e91fac60d9325a66a5981f8086c954ea2772efe28425eaf64"}, - {file = "coverage-6.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f467bbb837691ab5a8ca359199d3429a11a01e6dfb3d9dcc676dc035ca93c0a9"}, - {file = "coverage-6.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2641f803ee9f95b1f387f3e8f3bf28d83d9b69a39e9911e5bfee832bea75240d"}, - {file = "coverage-6.2-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:1219d760ccfafc03c0822ae2e06e3b1248a8e6d1a70928966bafc6838d3c9e48"}, - {file = "coverage-6.2-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:9a2b5b52be0a8626fcbffd7e689781bf8c2ac01613e77feda93d96184949a98e"}, - {file = "coverage-6.2-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:8e2c35a4c1f269704e90888e56f794e2d9c0262fb0c1b1c8c4ee44d9b9e77b5d"}, - {file = "coverage-6.2-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:5d6b09c972ce9200264c35a1d53d43ca55ef61836d9ec60f0d44273a31aa9f17"}, - {file = "coverage-6.2-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:e3db840a4dee542e37e09f30859f1612da90e1c5239a6a2498c473183a50e781"}, - {file = "coverage-6.2-cp36-cp36m-win32.whl", hash = "sha256:4e547122ca2d244f7c090fe3f4b5a5861255ff66b7ab6d98f44a0222aaf8671a"}, - {file = "coverage-6.2-cp36-cp36m-win_amd64.whl", hash = "sha256:01774a2c2c729619760320270e42cd9e797427ecfddd32c2a7b639cdc481f3c0"}, - {file = "coverage-6.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:fb8b8ee99b3fffe4fd86f4c81b35a6bf7e4462cba019997af2fe679365db0c49"}, - {file = "coverage-6.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:619346d57c7126ae49ac95b11b0dc8e36c1dd49d148477461bb66c8cf13bb521"}, - {file = "coverage-6.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0a7726f74ff63f41e95ed3a89fef002916c828bb5fcae83b505b49d81a066884"}, - {file = "coverage-6.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:cfd9386c1d6f13b37e05a91a8583e802f8059bebfccde61a418c5808dea6bbfa"}, - {file = "coverage-6.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:17e6c11038d4ed6e8af1407d9e89a2904d573be29d51515f14262d7f10ef0a64"}, - {file = "coverage-6.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:c254b03032d5a06de049ce8bca8338a5185f07fb76600afff3c161e053d88617"}, - {file = "coverage-6.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:dca38a21e4423f3edb821292e97cec7ad38086f84313462098568baedf4331f8"}, - {file = "coverage-6.2-cp37-cp37m-win32.whl", hash = "sha256:600617008aa82032ddeace2535626d1bc212dfff32b43989539deda63b3f36e4"}, - {file = "coverage-6.2-cp37-cp37m-win_amd64.whl", hash = "sha256:bf154ba7ee2fd613eb541c2bc03d3d9ac667080a737449d1a3fb342740eb1a74"}, - {file = "coverage-6.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f9afb5b746781fc2abce26193d1c817b7eb0e11459510fba65d2bd77fe161d9e"}, - {file = "coverage-6.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:edcada2e24ed68f019175c2b2af2a8b481d3d084798b8c20d15d34f5c733fa58"}, - {file = "coverage-6.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:a9c8c4283e17690ff1a7427123ffb428ad6a52ed720d550e299e8291e33184dc"}, - {file = "coverage-6.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f614fc9956d76d8a88a88bb41ddc12709caa755666f580af3a688899721efecd"}, - {file = "coverage-6.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:9365ed5cce5d0cf2c10afc6add145c5037d3148585b8ae0e77cc1efdd6aa2953"}, - {file = "coverage-6.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:8bdfe9ff3a4ea37d17f172ac0dff1e1c383aec17a636b9b35906babc9f0f5475"}, - {file = "coverage-6.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:63c424e6f5b4ab1cf1e23a43b12f542b0ec2e54f99ec9f11b75382152981df57"}, - {file = "coverage-6.2-cp38-cp38-win32.whl", hash = "sha256:49dbff64961bc9bdd2289a2bda6a3a5a331964ba5497f694e2cbd540d656dc1c"}, - {file = "coverage-6.2-cp38-cp38-win_amd64.whl", hash = "sha256:9a29311bd6429be317c1f3fe4bc06c4c5ee45e2fa61b2a19d4d1d6111cb94af2"}, - {file = "coverage-6.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:03b20e52b7d31be571c9c06b74746746d4eb82fc260e594dc662ed48145e9efd"}, - {file = "coverage-6.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:215f8afcc02a24c2d9a10d3790b21054b58d71f4b3c6f055d4bb1b15cecce685"}, - {file = "coverage-6.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:a4bdeb0a52d1d04123b41d90a4390b096f3ef38eee35e11f0b22c2d031222c6c"}, - {file = "coverage-6.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:c332d8f8d448ded473b97fefe4a0983265af21917d8b0cdcb8bb06b2afe632c3"}, - {file = "coverage-6.2-cp39-cp39-win32.whl", hash = "sha256:6e1394d24d5938e561fbeaa0cd3d356207579c28bd1792f25a068743f2d5b282"}, - {file = "coverage-6.2-cp39-cp39-win_amd64.whl", hash = "sha256:86f2e78b1eff847609b1ca8050c9e1fa3bd44ce755b2ec30e70f2d3ba3844644"}, - {file = "coverage-6.2-pp36.pp37.pp38-none-any.whl", hash = "sha256:5829192582c0ec8ca4a2532407bc14c2f338d9878a10442f5d03804a95fac9de"}, - {file = "coverage-6.2.tar.gz", hash = "sha256:e2cad8093172b7d1595b4ad66f24270808658e11acf43a8f95b41276162eb5b8"}, -] -docopt = [ - {file = "docopt-0.6.2.tar.gz", hash = "sha256:49b3a825280bd66b3aa83585ef59c4a8c82f2c8a522dbe754a8bc8d08c85c491"}, -] -importlib-metadata = [ - {file = "importlib_metadata-4.8.3-py3-none-any.whl", hash = "sha256:65a9576a5b2d58ca44d133c42a241905cc45e34d2c06fd5ba2bafa221e5d7b5e"}, - {file = "importlib_metadata-4.8.3.tar.gz", hash = "sha256:766abffff765960fcc18003801f7044eb6755ffae4521c8e8ce8e83b9c9b0668"}, -] -iniconfig = [ - {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, - {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, -] -packaging = [ - {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"}, - {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"}, -] -pluggy = [ - {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, - {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, -] -py = [ - {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, - {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, -] -pyparsing = [ - {file = "pyparsing-3.0.7-py3-none-any.whl", hash = "sha256:a6c06a88f252e6c322f65faf8f418b16213b51bdfaece0524c1c1bc30c63c484"}, - {file = "pyparsing-3.0.7.tar.gz", hash = "sha256:18ee9022775d270c55187733956460083db60b37d0d0fb357445f3094eed3eea"}, -] -pytest = [ - {file = "pytest-7.0.1-py3-none-any.whl", hash = "sha256:9ce3ff477af913ecf6321fe337b93a2c0dcf2a0a1439c43f5452112c1e4280db"}, - {file = "pytest-7.0.1.tar.gz", hash = "sha256:e30905a0c131d3d94b89624a1cc5afec3e0ba2fbdb151867d8e0ebd49850f171"}, -] -pytest-cov = [ - {file = "pytest-cov-3.0.0.tar.gz", hash = "sha256:e7f0f5b1617d2210a2cabc266dfe2f4c75a8d32fb89eafb7ad9d06f6d076d470"}, - {file = "pytest_cov-3.0.0-py3-none-any.whl", hash = "sha256:578d5d15ac4a25e5f961c938b85a05b09fdaae9deef3bb6de9a6e766622ca7a6"}, -] -pytest-html = [ - {file = "pytest-html-3.1.1.tar.gz", hash = "sha256:3ee1cf319c913d19fe53aeb0bc400e7b0bc2dbeb477553733db1dad12eb75ee3"}, - {file = "pytest_html-3.1.1-py3-none-any.whl", hash = "sha256:b7f82f123936a3f4d2950bc993c2c1ca09ce262c9ae12f9ac763a2401380b455"}, -] -pytest-metadata = [ - {file = "pytest-metadata-1.11.0.tar.gz", hash = "sha256:71b506d49d34e539cc3cfdb7ce2c5f072bea5c953320002c95968e0238f8ecf1"}, - {file = "pytest_metadata-1.11.0-py2.py3-none-any.whl", hash = "sha256:576055b8336dd4a9006dd2a47615f76f2f8c30ab12b1b1c039d99e834583523f"}, -] -pytest-mock = [ - {file = "pytest-mock-3.6.1.tar.gz", hash = "sha256:40217a058c52a63f1042f0784f62009e976ba824c418cced42e88d5f40ab0e62"}, - {file = "pytest_mock-3.6.1-py3-none-any.whl", hash = "sha256:30c2f2cc9759e76eee674b81ea28c9f0b94f8f0445a1b87762cadf774f0df7e3"}, -] -pytest-ordering = [ - {file = "pytest-ordering-0.6.tar.gz", hash = "sha256:561ad653626bb171da78e682f6d39ac33bb13b3e272d406cd555adb6b006bda6"}, - {file = "pytest_ordering-0.6-py2-none-any.whl", hash = "sha256:27fba3fc265f5d0f8597e7557885662c1bdc1969497cd58aff6ed21c3b617de2"}, - {file = "pytest_ordering-0.6-py3-none-any.whl", hash = "sha256:3f314a178dbeb6777509548727dc69edf22d6d9a2867bf2d310ab85c403380b6"}, -] -tomli = [ - {file = "tomli-1.2.3-py3-none-any.whl", hash = "sha256:e3069e4be3ead9668e21cb9b074cd948f7b3113fd9c8bba083f48247aab8b11c"}, - {file = "tomli-1.2.3.tar.gz", hash = "sha256:05b6166bff487dc068d322585c7ea4ef78deed501cc124060e0f238e89a9231f"}, -] -typing-extensions = [ - {file = "typing_extensions-4.1.1-py3-none-any.whl", hash = "sha256:21c85e0fe4b9a155d0799430b0ad741cdce7e359660ccbd8b530613e8df88ce2"}, - {file = "typing_extensions-4.1.1.tar.gz", hash = "sha256:1a9462dcc3347a79b1f1c0271fbe79e844580bb598bafa1ed208b94da3cdcd42"}, -] -zipp = [ - {file = "zipp-3.6.0-py3-none-any.whl", hash = "sha256:9fe5ea21568a0a70e50f273397638d39b03353731e6cbbb3fd8502a33fec40bc"}, - {file = "zipp-3.6.0.tar.gz", hash = "sha256:71c644c5369f4a6e07636f0aa966270449561fcea2e3d6747b8d23efaa9d7832"}, -] +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 = [] diff --git a/pyproject.toml b/pyproject.toml index 6495880e..6708559a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,6 +21,7 @@ packages = [ [tool.poetry.dependencies] python = "^3.6" docopt = "^0.6.2" +websocket-client = "^1.2.3" [tool.poetry.dev-dependencies] pytest = ">=5.2" diff --git a/stomp/__init__.py b/stomp/__init__.py index 967fb185..23b0c69b 100644 --- a/stomp/__init__.py +++ b/stomp/__init__.py @@ -33,6 +33,8 @@ # Connection12 = connect.StompConnection12 StompConnection12 = Connection12 +Connection12WS = connect.StompConnection12WS +StompConnection12WS = Connection12WS ## # Default connection alias (STOMP 1.1). diff --git a/stomp/connect.py b/stomp/connect.py index c1858f1d..a2644f42 100644 --- a/stomp/connect.py +++ b/stomp/connect.py @@ -213,3 +213,39 @@ def disconnect(self, receipt=None, headers=None, **keyword_headers): @staticmethod def is_eol(c): return c == b"\x0a" or c == b"\x0d\x0a" + + +class StompConnection12WS(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) diff --git a/stomp/transport.py b/stomp/transport.py index 165aded6..e454d695 100644 --- a/stomp/transport.py +++ b/stomp/transport.py @@ -11,6 +11,7 @@ import warnings from io import BytesIO from time import monotonic +import websocket try: from socket import SOL_SOCKET, SO_KEEPALIVE, SOL_TCP, TCP_KEEPIDLE, TCP_KEEPINTVL, TCP_KEEPCNT @@ -871,3 +872,392 @@ def get_ssl(self, host_and_port=None): host_and_port = self.current_host_and_port return self.__ssl_params.get(host_and_port) + + +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 exception.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 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): + 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 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) diff --git a/tests/setup.ini b/tests/setup.ini index a066f81e..b588133c 100644 --- a/tests/setup.ini +++ b/tests/setup.ini @@ -15,6 +15,12 @@ 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 @@ -27,4 +33,4 @@ ssl_port = 65001 host = 172.17.0.2 port = 61615 user = testuser -password = password \ No newline at end of file +password = password diff --git a/tests/test_rabbitmq_ws.py b/tests/test_rabbitmq_ws.py new file mode 100644 index 00000000..f28fcc59 --- /dev/null +++ b/tests/test_rabbitmq_ws.py @@ -0,0 +1,34 @@ +import stomp +from stomp.listener import TestListener, WaitingListener +from .testutils import * +import time + + +@pytest.fixture() +def conn(): + conn = stomp.Connection12WS(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 diff --git a/tests/testutils.py b/tests/testutils.py index 29a5f3a9..83eb372f 100644 --- a/tests/testutils.py +++ b/tests/testutils.py @@ -94,6 +94,12 @@ def get_rabbitmq_host(): 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