├── .github ├── ISSUE_TEMPLATE.md └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── .travis.yml ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── LICENSE-MPL-RabbitMQ ├── Makefile ├── NOTES ├── README.md ├── erlang.mk ├── examples ├── perl │ ├── rabbitmq_stomp_recv.pl │ ├── rabbitmq_stomp_rpc_client.pl │ ├── rabbitmq_stomp_rpc_service.pl │ ├── rabbitmq_stomp_send.pl │ ├── rabbitmq_stomp_send_many.pl │ └── rabbitmq_stomp_slow_recv.pl └── ruby │ ├── amq-sender.rb │ ├── cb-receiver.rb │ ├── cb-sender.rb │ ├── cb-slow-receiver.rb │ ├── exchange-receiver.rb │ ├── exchange-sender.rb │ ├── persistent-receiver.rb │ ├── persistent-sender.rb │ ├── temp-queue-client.rb │ ├── temp-queue-service.rb │ ├── topic-broadcast-receiver.rb │ ├── topic-broadcast-with-unsubscribe.rb │ └── topic-sender.rb ├── include ├── rabbit_stomp.hrl ├── rabbit_stomp_frame.hrl └── rabbit_stomp_headers.hrl ├── priv └── schema │ └── rabbitmq_stomp.schema ├── rabbitmq-components.mk ├── src ├── Elixir.RabbitMQ.CLI.Ctl.Commands.ListStompConnectionsCommand.erl ├── rabbit_stomp.erl ├── rabbit_stomp_client_sup.erl ├── rabbit_stomp_connection_info.erl ├── rabbit_stomp_frame.erl ├── rabbit_stomp_internal_event_handler.erl ├── rabbit_stomp_processor.erl ├── rabbit_stomp_reader.erl ├── rabbit_stomp_sup.erl └── rabbit_stomp_util.erl └── test ├── amqqueue_SUITE.erl ├── command_SUITE.erl ├── config_schema_SUITE.erl ├── config_schema_SUITE_data ├── certs │ ├── cacert.pem │ ├── cert.pem │ └── key.pem └── rabbitmq_stomp.snippets ├── connections_SUITE.erl ├── frame_SUITE.erl ├── proxy_protocol_SUITE.erl ├── python_SUITE.erl ├── python_SUITE_data ├── deps │ ├── pika │ │ └── Makefile │ └── stomppy │ │ └── Makefile └── src │ ├── ack.py │ ├── amqp_headers.py │ ├── base.py │ ├── connect_options.py │ ├── destinations.py │ ├── errors.py │ ├── lifecycle.py │ ├── parsing.py │ ├── queue_properties.py │ ├── redelivered.py │ ├── reliability.py │ ├── ssl_lifecycle.py │ ├── test.py │ ├── test_connect_options.py │ ├── test_runner.py │ ├── test_ssl.py │ ├── test_util.py │ ├── topic_permissions.py │ ├── transactions.py │ ├── x_queue_name.py │ └── x_queue_type_quorum.py ├── src ├── rabbit_stomp_client.erl ├── rabbit_stomp_publish_test.erl └── test.config ├── topic_SUITE.erl └── util_SUITE.erl /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | Thank you for using RabbitMQ and for taking the time to report an 2 | issue. 3 | 4 | ## Does This Belong to GitHub or RabbitMQ Mailing List? 5 | 6 | *Important:* please first read the `CONTRIBUTING.md` document in the 7 | root of this repository. It will help you determine whether your 8 | feedback should be directed to the RabbitMQ mailing list [1] instead. 9 | 10 | ## Please Help Maintainers and Contributors Help You 11 | 12 | In order for the RabbitMQ team to investigate your issue, please provide 13 | **as much as possible** of the following details: 14 | 15 | * RabbitMQ version 16 | * Erlang version 17 | * RabbitMQ server and client application log files 18 | * A runnable code sample, terminal transcript or detailed set of 19 | instructions that can be used to reproduce the issue 20 | * RabbitMQ plugin information via `rabbitmq-plugins list` 21 | * Client library version (for all libraries used) 22 | * Operating system, version, and patch level 23 | 24 | Running the `rabbitmq-collect-env` [2] script can provide most of the 25 | information needed. Please make the archive available via a third-party 26 | service and note that **the script does not attempt to scrub any 27 | sensitive data**. 28 | 29 | If your issue involves RabbitMQ management UI or HTTP API, please also provide 30 | the following: 31 | 32 | * Browser and its version 33 | * What management UI page was used (if applicable) 34 | * How the HTTP API requests performed can be reproduced with `curl` 35 | * Operating system on which you are running your browser, and its version 36 | * Errors reported in the JavaScript console (if any) 37 | 38 | This information **greatly speeds up issue investigation** (or makes it 39 | possible to investigate it at all). Please help project maintainers and 40 | contributors to help you by providing it! 41 | 42 | 1. https://groups.google.com/forum/#!forum/rabbitmq-users 43 | 2. https://github.com/rabbitmq/support-tools/blob/master/scripts/rabbitmq-collect-env 44 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Proposed Changes 2 | 3 | Please describe the big picture of your changes here to communicate to the 4 | RabbitMQ team why we should accept this pull request. If it fixes a bug or 5 | resolves a feature request, be sure to link to that issue. 6 | 7 | A pull request that doesn't explain **why** the change was made has a much 8 | lower chance of being accepted. 9 | 10 | If English isn't your first language, don't worry about it and try to 11 | communicate the problem you are trying to solve to the best of your abilities. 12 | As long as we can understand the intent, it's all good. 13 | 14 | ## Types of Changes 15 | 16 | What types of changes does your code introduce to this project? 17 | _Put an `x` in the boxes that apply_ 18 | 19 | - [ ] Bugfix (non-breaking change which fixes issue #NNNN) 20 | - [ ] New feature (non-breaking change which adds functionality) 21 | - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) 22 | - [ ] Documentation (correction or otherwise) 23 | - [ ] Cosmetics (whitespace, appearance) 24 | 25 | ## Checklist 26 | 27 | _Put an `x` in the boxes that apply. You can also fill these out after creating 28 | the PR. If you're unsure about any of them, don't hesitate to ask on the 29 | mailing list. We're here to help! This is simply a reminder of what we are 30 | going to look for before merging your code._ 31 | 32 | - [ ] I have read the `CONTRIBUTING.md` document 33 | - [ ] I have signed the CA (see https://cla.pivotal.io/sign/rabbitmq) 34 | - [ ] All tests pass locally with my changes 35 | - [ ] I have added tests that prove my fix is effective or that my feature works 36 | - [ ] I have added necessary documentation (if appropriate) 37 | - [ ] Any dependent changes have been merged and published in related repositories 38 | 39 | ## Further Comments 40 | 41 | If this is a relatively large or complex change, kick off the discussion by 42 | explaining why you chose the solution you did and what alternatives you 43 | considered, etc. 44 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .sw? 2 | .*.sw? 3 | *.beam 4 | /.erlang.mk/ 5 | /cover/ 6 | /debug/ 7 | /deps/ 8 | /doc/ 9 | /ebin/ 10 | /escript/ 11 | /escript.lock 12 | /logs/ 13 | /plugins/ 14 | /plugins.lock 15 | /sbin/ 16 | /sbin.lock 17 | /xrefr 18 | 19 | rabbitmq_stomp.d 20 | 21 | # Python testsuite. 22 | .python-version 23 | *.pyc 24 | test/python_SUITE_data/deps/pika/pika/ 25 | test/python_SUITE_data/deps/pika/pika-*/ 26 | test/python_SUITE_data/deps/stomppy/stomppy/ 27 | test/python_SUITE_data/deps/stomppy/stomppy-git/ 28 | 29 | test/config_schema_SUITE_data/schema/ 30 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # vim:sw=2:et: 2 | 3 | os: linux 4 | dist: xenial 5 | language: elixir 6 | notifications: 7 | email: 8 | recipients: 9 | - alerts@rabbitmq.com 10 | on_success: never 11 | on_failure: always 12 | addons: 13 | apt: 14 | packages: 15 | - awscli 16 | cache: 17 | apt: true 18 | env: 19 | global: 20 | - secure: oLN5hBjMeKvT365DSoNLPPIZ9Bf9gxEgP3EJCZgPgKVvsE+4DhosdwYPxo1mNA2mq+6soizNGiW5LlD92UZonNgptl7UDwmVFWSHawEopYz67zFbcohEeHnKFr5bAapGgttdAHkfWH5nxv90O6OfEva0QBXkQb8O/hOdmYsVYOs= 21 | - secure: efpmC/exFPHVbK4peAI4hAi7WKb5eUPgqhax95iDF54aVbt6SuO4h/t4gC2eiKU9el4YEccmapHfJyQ5FZSEw+aWS0wAXpmXlbIc8rxKuWbESeqvGKTcDmILfcLJYXt/B3pNzynRQCPUJkYo946j18+kfzB+cBHm7TV021hnt9w= 22 | 23 | # $base_rmq_ref is used by rabbitmq-components.mk to select the 24 | # appropriate branch for dependencies. 25 | - base_rmq_ref=master 26 | 27 | elixir: 28 | - '1.9' 29 | otp_release: 30 | - '21.3' 31 | - '22.2' 32 | 33 | install: 34 | # This project being an Erlang one (we just set language to Elixir 35 | # to ensure it is installed), we don't want Travis to run mix(1) 36 | # automatically as it will break. 37 | skip 38 | 39 | script: 40 | # $current_rmq_ref is also used by rabbitmq-components.mk to select 41 | # the appropriate branch for dependencies. 42 | - make check-rabbitmq-components.mk 43 | current_rmq_ref="${TRAVIS_PULL_REQUEST_BRANCH:-${TRAVIS_BRANCH}}" 44 | - make xref 45 | current_rmq_ref="${TRAVIS_PULL_REQUEST_BRANCH:-${TRAVIS_BRANCH}}" 46 | - make tests 47 | current_rmq_ref="${TRAVIS_PULL_REQUEST_BRANCH:-${TRAVIS_BRANCH}}" 48 | 49 | after_failure: 50 | - | 51 | cd "$TRAVIS_BUILD_DIR" 52 | if test -d logs && test "$AWS_ACCESS_KEY_ID" && test "$AWS_SECRET_ACCESS_KEY"; then 53 | archive_name="$(basename "$TRAVIS_REPO_SLUG")-$TRAVIS_JOB_NUMBER" 54 | 55 | tar -c --transform "s/^logs/${archive_name}/" -f - logs | \ 56 | xz > "${archive_name}.tar.xz" 57 | 58 | aws s3 cp "${archive_name}.tar.xz" s3://server-release-pipeline/travis-ci-logs/ \ 59 | --region eu-west-1 \ 60 | --acl public-read 61 | fi 62 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Code of Conduct 2 | 3 | As contributors and maintainers of this project, and in the interest of fostering an open 4 | and welcoming community, we pledge to respect all people who contribute through reporting 5 | issues, posting feature requests, updating documentation, submitting pull requests or 6 | patches, and other activities. 7 | 8 | We are committed to making participation in this project a harassment-free experience for 9 | everyone, regardless of level of experience, gender, gender identity and expression, 10 | sexual orientation, disability, personal appearance, body size, race, ethnicity, age, 11 | religion, or nationality. 12 | 13 | Examples of unacceptable behavior by participants include: 14 | 15 | * The use of sexualized language or imagery 16 | * Personal attacks 17 | * Trolling or insulting/derogatory comments 18 | * Public or private harassment 19 | * Publishing other's private information, such as physical or electronic addresses, 20 | without explicit permission 21 | * Other unethical or unprofessional conduct 22 | 23 | Project maintainers have the right and responsibility to remove, edit, or reject comments, 24 | commits, code, wiki edits, issues, and other contributions that are not aligned to this 25 | Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors 26 | that they deem inappropriate, threatening, offensive, or harmful. 27 | 28 | By adopting this Code of Conduct, project maintainers commit themselves to fairly and 29 | consistently applying these principles to every aspect of managing this project. Project 30 | maintainers who do not follow or enforce the Code of Conduct may be permanently removed 31 | from the project team. 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an 34 | individual is representing the project or its community. 35 | 36 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by 37 | contacting a project maintainer at [info@rabbitmq.com](mailto:info@rabbitmq.com). All complaints will 38 | be reviewed and investigated and will result in a response that is deemed necessary and 39 | appropriate to the circumstances. Maintainers are obligated to maintain confidentiality 40 | with regard to the reporter of an incident. 41 | 42 | This Code of Conduct is adapted from the 43 | [Contributor Covenant](https://contributor-covenant.org), version 1.3.0, available at 44 | [contributor-covenant.org/version/1/3/0/](https://contributor-covenant.org/version/1/3/0/) 45 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Thank you for using RabbitMQ and for taking the time to contribute to the project. 2 | This document has two main parts: 3 | 4 | * when and how to file GitHub issues for RabbitMQ projects 5 | * how to submit pull requests 6 | 7 | They intend to save you and RabbitMQ maintainers some time, so please 8 | take a moment to read through them. 9 | 10 | ## Overview 11 | 12 | ### GitHub issues 13 | 14 | The RabbitMQ team uses GitHub issues for _specific actionable items_ that 15 | engineers can work on. This assumes the following: 16 | 17 | * GitHub issues are not used for questions, investigations, root cause 18 | analysis, discussions of potential issues, etc (as defined by this team) 19 | * Enough information is provided by the reporter for maintainers to work with 20 | 21 | The team receives many questions through various venues every single 22 | day. Frequently, these questions do not include the necessary details 23 | the team needs to begin useful work. GitHub issues can very quickly 24 | turn into a something impossible to navigate and make sense 25 | of. Because of this, questions, investigations, root cause analysis, 26 | and discussions of potential features are all considered to be 27 | [mailing list][rmq-users] material. If you are unsure where to begin, 28 | the [RabbitMQ users mailing list][rmq-users] is the right place. 29 | 30 | Getting all the details necessary to reproduce an issue, make a 31 | conclusion or even form a hypothesis about what's happening can take a 32 | fair amount of time. Please help others help you by providing a way to 33 | reproduce the behavior you're observing, or at least sharing as much 34 | relevant information as possible on the [RabbitMQ users mailing 35 | list][rmq-users]. 36 | 37 | Please provide versions of the software used: 38 | 39 | * RabbitMQ server 40 | * Erlang 41 | * Operating system version (and distribution, if applicable) 42 | * All client libraries used 43 | * RabbitMQ plugins (if applicable) 44 | 45 | The following information greatly helps in investigating and reproducing issues: 46 | 47 | * RabbitMQ server logs 48 | * A code example or terminal transcript that can be used to reproduce 49 | * Full exception stack traces (a single line message is not enough!) 50 | * `rabbitmqctl report` and `rabbitmqctl environment` output 51 | * Other relevant details about the environment and workload, e.g. a traffic capture 52 | * Feel free to edit out hostnames and other potentially sensitive information. 53 | 54 | To make collecting much of this and other environment information, use 55 | the [`rabbitmq-collect-env`][rmq-collect-env] script. It will produce an archive with 56 | server logs, operating system logs, output of certain diagnostics commands and so on. 57 | Please note that **no effort is made to scrub any information that may be sensitive**. 58 | 59 | ### Pull Requests 60 | 61 | RabbitMQ projects use pull requests to discuss, collaborate on and accept code contributions. 62 | Pull requests is the primary place of discussing code changes. 63 | 64 | Here's the recommended workflow: 65 | 66 | * [Fork the repository][github-fork] or repositories you plan on contributing to. If multiple 67 | repositories are involved in addressing the same issue, please use the same branch name 68 | in each repository 69 | * Create a branch with a descriptive name in the relevant repositories 70 | * Make your changes, run tests (usually with `make tests`), commit with a 71 | [descriptive message][git-commit-msgs], push to your fork 72 | * Submit pull requests with an explanation what has been changed and **why** 73 | * Submit a filled out and signed [Contributor Agreement][ca-agreement] if needed (see below) 74 | * Be patient. We will get to your pull request eventually 75 | 76 | If what you are going to work on is a substantial change, please first 77 | ask the core team for their opinion on the [RabbitMQ users mailing list][rmq-users]. 78 | 79 | ## Code of Conduct 80 | 81 | See [CODE_OF_CONDUCT.md](./CODE_OF_CONDUCT.md). 82 | 83 | ## Contributor Agreement 84 | 85 | If you want to contribute a non-trivial change, please submit a signed 86 | copy of our [Contributor Agreement][ca-agreement] around the time you 87 | submit your pull request. This will make it much easier (in some 88 | cases, possible) for the RabbitMQ team at Pivotal to merge your 89 | contribution. 90 | 91 | ## Where to Ask Questions 92 | 93 | If something isn't clear, feel free to ask on our [mailing list][rmq-users]. 94 | 95 | [rmq-collect-env]: https://github.com/rabbitmq/support-tools/blob/master/scripts/rabbitmq-collect-env 96 | [git-commit-msgs]: https://chris.beams.io/posts/git-commit/ 97 | [rmq-users]: https://groups.google.com/forum/#!forum/rabbitmq-users 98 | [ca-agreement]: https://cla.pivotal.io/sign/rabbitmq 99 | [github-fork]: https://help.github.com/articles/fork-a-repo/ 100 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This package is licensed under the MPL 2.0. For the MPL 2.0, please see LICENSE-MPL-RabbitMQ. 2 | 3 | If you have any questions regarding licensing, please contact us at 4 | info@rabbitmq.com. 5 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PROJECT = rabbitmq_stomp 2 | PROJECT_DESCRIPTION = RabbitMQ STOMP plugin 3 | PROJECT_MOD = rabbit_stomp 4 | 5 | define PROJECT_ENV 6 | [ 7 | {default_user, 8 | [{login, <<"guest">>}, 9 | {passcode, <<"guest">>}]}, 10 | {default_vhost, <<"/">>}, 11 | {default_topic_exchange, <<"amq.topic">>}, 12 | {default_nack_requeue, true}, 13 | {ssl_cert_login, false}, 14 | {implicit_connect, false}, 15 | {tcp_listeners, [61613]}, 16 | {ssl_listeners, []}, 17 | {num_tcp_acceptors, 10}, 18 | {num_ssl_acceptors, 10}, 19 | {tcp_listen_options, [{backlog, 128}, 20 | {nodelay, true}]}, 21 | %% see rabbitmq/rabbitmq-stomp#39 22 | {trailing_lf, true}, 23 | %% see rabbitmq/rabbitmq-stomp#57 24 | {hide_server_info, false}, 25 | {proxy_protocol, false} 26 | ] 27 | endef 28 | 29 | define PROJECT_APP_EXTRA_KEYS 30 | {broker_version_requirements, []} 31 | endef 32 | 33 | DEPS = ranch rabbit_common rabbit amqp_client 34 | TEST_DEPS = rabbitmq_ct_helpers rabbitmq_ct_client_helpers 35 | 36 | DEP_EARLY_PLUGINS = rabbit_common/mk/rabbitmq-early-plugin.mk 37 | DEP_PLUGINS = rabbit_common/mk/rabbitmq-plugin.mk 38 | 39 | # FIXME: Use erlang.mk patched for RabbitMQ, while waiting for PRs to be 40 | # reviewed and merged. 41 | 42 | ERLANG_MK_REPO = https://github.com/rabbitmq/erlang.mk.git 43 | ERLANG_MK_COMMIT = rabbitmq-tmp 44 | 45 | include rabbitmq-components.mk 46 | include erlang.mk 47 | -------------------------------------------------------------------------------- /NOTES: -------------------------------------------------------------------------------- 1 | Comments from Sean Treadway, 2 June 2008, on the rabbitmq-discuss list: 2 | 3 | - On naming, extensibility, and headers: 4 | 5 | "STOMP looked like it was MQ agnostic and extensible while keeping 6 | the core headers well defined (ack=client, message_id, etc...), 7 | but my application was not MQ agnostic. Plus I saw some of the 8 | ActiveMQ headers weren't available or necessary in RabbitMQ. 9 | 10 | "Keeping the AMQP naming is the best way to piggy back on the AMQP 11 | documentation. For those that need simple, transient queues, the 12 | existing STOMP documentation would be sufficient." 13 | 14 | ... 15 | 16 | "I only have experience with RabbitMQ, so I'm fine with exposing 17 | AMQP rather than try to come to some agreement over the extension 18 | names of standard STOMP headers." 19 | 20 | - On queue deletion over STOMP: 21 | 22 | "Here, I would stick with the verbs defined in STOMP and extend the 23 | verbs with headers. One possibility is to use UNSUBSCRIBE 24 | messages to change the queue properties before sending the 25 | 'basic.cancel' method. Another possibility is to change queue 26 | properties on a SUBSCRIBE message. Neither seem nice to me. Third 27 | option is to do nothing, and delete the queues outside of the 28 | STOMP protocol" 29 | 30 | Comments from Darien Kindlund, 11 February 2009, on the rabbitmq-discuss list: 31 | 32 | - On testing of connection establishment: 33 | 34 | "[O]nce I switched each perl process over to re-using their 35 | existing STOMP connection, things worked much, much better. As 36 | such, I'm continuing development. In your unit testing, you may 37 | want to include rapid connect/disconnect behavior or otherwise 38 | explicitly warn developers to avoid this scenario." 39 | 40 | Comments from Novak Joe, 11 September 2008, on the rabbitmq-discuss list: 41 | 42 | - On broadcast send: 43 | 44 | "That said, I think it would also be useful to add to the STOMP 45 | wiki page an additional note on broadcast SEND. In particular I 46 | found that in order to send a message to a broadcast exchange it 47 | needs look something like: 48 | 49 | --------------------------------- 50 | SEND 51 | destination:x.mytopic 52 | exchange:amq.topic 53 | 54 | my message 55 | \x00 56 | -------------------------------- 57 | 58 | "However my initial newb intuition was that it should look more like: 59 | 60 | --------------------------------- 61 | SEND 62 | destination: 63 | exchange:amq.topic 64 | routing_key:x.mytopic 65 | 66 | my message 67 | \x00 68 | -------------------------------- 69 | 70 | "The ruby examples cleared this up but not before I experienced a 71 | bit of confusion on the subject." 72 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RabbitMQ STOMP adapter 2 | 3 | ## This was migrated to https://github.com/rabbitmq/rabbitmq-server 4 | 5 | This repository has been moved to the main unified RabbitMQ "monorepo", including all open issues. You can find the source under [/deps/rabbitmq_stomp](https://github.com/rabbitmq/rabbitmq-server/tree/master/deps/rabbitmq_stomp). 6 | All issues have been transferred. 7 | 8 | ## Overview 9 | 10 | The STOMP adapter is included in the RabbitMQ distribution. To enable 11 | it, use [rabbitmq-plugins](https://www.rabbitmq.com/man/rabbitmq-plugins.1.man.html): 12 | 13 | rabbitmq-plugins enable rabbitmq_stomp 14 | 15 | ## Supported STOMP Versions 16 | 17 | 1.0 through 1.2. 18 | 19 | ## Documentation 20 | 21 | [RabbitMQ STOMP plugin documentation](https://www.rabbitmq.com/stomp.html). 22 | 23 | ## Continuous Integration 24 | 25 | [![Build Status](https://travis-ci.org/rabbitmq/rabbitmq-stomp.svg?branch=master)](https://travis-ci.org/rabbitmq/rabbitmq-stomp) 26 | -------------------------------------------------------------------------------- /examples/perl/rabbitmq_stomp_recv.pl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl -w 2 | # subscribe to messages from the queue 'foo' 3 | use Net::Stomp; 4 | my $stomp = Net::Stomp->new({hostname=>'localhost', port=>'61613'}); 5 | $stomp->connect({login=>'guest', passcode=>'guest'}); 6 | $stomp->subscribe({'destination'=>'/queue/foo', 'ack'=>'client'}); 7 | while (1) { 8 | my $frame = $stomp->receive_frame; 9 | print $frame->body . "\n"; 10 | $stomp->ack({frame=>$frame}); 11 | last if $frame->body eq 'QUIT'; 12 | } 13 | $stomp->disconnect; 14 | -------------------------------------------------------------------------------- /examples/perl/rabbitmq_stomp_rpc_client.pl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl -w 2 | 3 | use Net::Stomp; 4 | my $stomp = Net::Stomp->new({hostname=>'localhost', port=>'61613'}); 5 | $stomp->connect({login=>'guest', passcode=>'guest'}); 6 | 7 | my $private_q_name = "/temp-queue/test"; 8 | 9 | $stomp->send({destination => '/queue/rabbitmq_stomp_rpc_service', 10 | 'reply-to' => $private_q_name, 11 | body => "request from $private_q_name"}); 12 | print "Reply: " . $stomp->receive_frame->body . "\n"; 13 | 14 | $stomp->disconnect; 15 | -------------------------------------------------------------------------------- /examples/perl/rabbitmq_stomp_rpc_service.pl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl -w 2 | 3 | use Net::Stomp; 4 | 5 | my $stomp = Net::Stomp->new({hostname=>'localhost', port=>'61613'}); 6 | $stomp->connect({login=>'guest', passcode=>'guest'}); 7 | 8 | $stomp->subscribe({'destination'=>'/queue/rabbitmq_stomp_rpc_service', 'ack'=>'client'}); 9 | while (1) { 10 | print "Waiting for request...\n"; 11 | my $frame = $stomp->receive_frame; 12 | print "Received message, reply_to = " . $frame->headers->{"reply-to"} . "\n"; 13 | print $frame->body . "\n"; 14 | 15 | $stomp->send({destination => $frame->headers->{"reply-to"}, bytes_message => 1, 16 | body => "Got body: " . $frame->body}); 17 | $stomp->ack({frame=>$frame}); 18 | last if $frame->body eq 'QUIT'; 19 | } 20 | 21 | $stomp->disconnect; 22 | -------------------------------------------------------------------------------- /examples/perl/rabbitmq_stomp_send.pl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl -w 2 | # send a message to the queue 'foo' 3 | use Net::Stomp; 4 | my $stomp = Net::Stomp->new({hostname=>'localhost', port=>'61613'}); 5 | $stomp->connect({login=>'guest', passcode=>'guest'}); 6 | $stomp->send({destination=>'/exchange/amq.fanout', 7 | bytes_message=>1, 8 | body=>($ARGV[0] or "test\0message")}); 9 | $stomp->disconnect; 10 | -------------------------------------------------------------------------------- /examples/perl/rabbitmq_stomp_send_many.pl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl -w 2 | # send a message to the queue 'foo' 3 | use Net::Stomp; 4 | my $stomp = Net::Stomp->new({hostname=>'localhost', port=>'61613'}); 5 | $stomp->connect({login=>'guest', passcode=>'guest'}); 6 | for (my $i = 0; $i < 10000; $i++) { 7 | $stomp->send({destination=>'/queue/foo', 8 | bytes_message=>1, 9 | body=>($ARGV[0] or "message $i")}); 10 | } 11 | $stomp->disconnect; 12 | -------------------------------------------------------------------------------- /examples/perl/rabbitmq_stomp_slow_recv.pl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl -w 2 | # subscribe to messages from the queue 'foo' 3 | use Net::Stomp; 4 | my $stomp = Net::Stomp->new({hostname=>'localhost', port=>'61613'}); 5 | $stomp->connect({login=>'guest', passcode=>'guest', prefetch=>1}); 6 | $stomp->subscribe({'destination'=>'/queue/foo', 'ack'=>'client'}); 7 | while (1) { 8 | my $frame = $stomp->receive_frame; 9 | print $frame->body . "\n"; 10 | sleep 1; 11 | $stomp->ack({frame=>$frame}); 12 | last if $frame->body eq 'QUIT'; 13 | } 14 | $stomp->disconnect; 15 | -------------------------------------------------------------------------------- /examples/ruby/amq-sender.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'stomp' 3 | 4 | client = Stomp::Client.new("guest", "guest", "localhost", 61613) 5 | 6 | # This publishes a message to a queue named 'amq-test' which is managed by AMQP broker. 7 | client.publish("/amq/queue/amq-test", "test-message") 8 | 9 | # close this connection 10 | client.close 11 | -------------------------------------------------------------------------------- /examples/ruby/cb-receiver.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'stomp' 3 | 4 | conn = Stomp::Connection.open('guest', 'guest', 'localhost') 5 | conn.subscribe('/queue/carl') 6 | while mesg = conn.receive 7 | puts mesg.body 8 | end 9 | -------------------------------------------------------------------------------- /examples/ruby/cb-sender.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'stomp' 3 | 4 | client = Stomp::Client.new("guest", "guest", "localhost", 61613) 5 | 10000.times { |i| client.publish '/queue/carl', "Test Message number #{i}"} 6 | client.publish '/queue/carl', "All Done!" 7 | -------------------------------------------------------------------------------- /examples/ruby/cb-slow-receiver.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'stomp' 3 | 4 | # Note: requires support for connect_headers hash in the STOMP gem's connection.rb 5 | conn = Stomp::Connection.open('guest', 'guest', 'localhost', 61613, false, 5, {:prefetch => 1}) 6 | conn.subscribe('/queue/carl', {:ack => 'client'}) 7 | while mesg = conn.receive 8 | puts mesg.body 9 | puts 'Sleeping...' 10 | sleep 0.2 11 | puts 'Awake again. Acking.' 12 | conn.ack mesg.headers['message-id'] 13 | end 14 | -------------------------------------------------------------------------------- /examples/ruby/exchange-receiver.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'stomp' 3 | 4 | conn = Stomp::Connection.open("guest", "guest", "localhost") 5 | conn.subscribe '/exchange/amq.fanout/test' 6 | 7 | puts "Waiting for messages..." 8 | 9 | begin 10 | while mesg = conn.receive 11 | puts mesg.body 12 | end 13 | rescue Exception => _ 14 | conn.disconnect 15 | end 16 | -------------------------------------------------------------------------------- /examples/ruby/exchange-sender.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'stomp' 3 | 4 | client = Stomp::Client.new("guest", "guest", "localhost", 61613) 5 | 6 | # This publishes a message to the 'amq.fanout' exchange which is managed by 7 | # AMQP broker and specifies routing-key of 'test'. You can get other exchanges 8 | # through 'list_exchanges' subcommand of 'rabbitmqctl' utility. 9 | client.publish("/exchange/amq.fanout/test", "test message") 10 | 11 | # close this connection 12 | client.close 13 | -------------------------------------------------------------------------------- /examples/ruby/persistent-receiver.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'stomp' 3 | 4 | conn = Stomp::Connection.open('guest', 'guest', 'localhost') 5 | conn.subscribe('/queue/durable', :'auto-delete' => false, :durable => true) 6 | 7 | puts "Waiting for messages..." 8 | 9 | while mesg = conn.receive 10 | puts mesg.body 11 | end 12 | -------------------------------------------------------------------------------- /examples/ruby/persistent-sender.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'stomp' 3 | 4 | # Use this case to test durable queues 5 | # 6 | # Start the sender - 11 messages will be sent to /queue/durable and the sender exits 7 | # Stop the server - 11 messages will be written to disk 8 | # Start the server 9 | # Start the receiver - 11 messages should be received and the receiver - interrupt the receive loop 10 | 11 | client = Stomp::Client.new("guest", "guest", "localhost", 61613) 12 | 10.times { |i| client.publish '/queue/durable', "Test Message number #{i} sent at #{Time.now}", 'delivery-mode' => '2'} 13 | client.publish '/queue/durable', "All Done!" 14 | -------------------------------------------------------------------------------- /examples/ruby/temp-queue-client.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'stomp' 3 | 4 | conn = Stomp::Connection.open("guest", "guest", "localhost") 5 | conn.publish("/queue/rpc-service", "test message", { 6 | 'reply-to' => '/temp-queue/test' 7 | }) 8 | puts conn.receive.body 9 | conn.disconnect 10 | -------------------------------------------------------------------------------- /examples/ruby/temp-queue-service.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'stomp' 3 | 4 | conn = Stomp::Connection.open("guest", "guest", "localhost") 5 | conn.subscribe '/queue/rpc-service' 6 | 7 | begin 8 | while mesg = conn.receive 9 | puts "received message and replies to #{mesg.headers['reply-to']}" 10 | 11 | conn.publish(mesg.headers['reply-to'], '(reply) ' + mesg.body) 12 | end 13 | rescue Exception => _ 14 | conn.disconnect 15 | end 16 | -------------------------------------------------------------------------------- /examples/ruby/topic-broadcast-receiver.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'stomp' 3 | 4 | topic = ARGV[0] || 'x' 5 | puts "Binding to /topic/#{topic}" 6 | 7 | conn = Stomp::Connection.open('guest', 'guest', 'localhost') 8 | conn.subscribe("/topic/#{topic}") 9 | while mesg = conn.receive 10 | puts mesg.body 11 | end 12 | -------------------------------------------------------------------------------- /examples/ruby/topic-broadcast-with-unsubscribe.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'stomp' # this is a gem 3 | 4 | conn = Stomp::Connection.open('guest', 'guest', 'localhost') 5 | puts "Subscribing to /topic/x" 6 | conn.subscribe('/topic/x') 7 | puts 'Receiving...' 8 | mesg = conn.receive 9 | puts mesg.body 10 | puts "Unsubscribing from /topic/x" 11 | conn.unsubscribe('/topic/x') 12 | puts 'Sleeping 5 seconds...' 13 | sleep 5 14 | -------------------------------------------------------------------------------- /examples/ruby/topic-sender.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'stomp' 3 | 4 | client = Stomp::Client.new("guest", "guest", "localhost", 61613) 5 | client.publish '/topic/x.y', 'first message' 6 | client.publish '/topic/x.z', 'second message' 7 | client.publish '/topic/x', 'third message' 8 | -------------------------------------------------------------------------------- /include/rabbit_stomp.hrl: -------------------------------------------------------------------------------- 1 | %% This Source Code Form is subject to the terms of the Mozilla Public 2 | %% License, v. 2.0. If a copy of the MPL was not distributed with this 3 | %% file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | %% 5 | %% Copyright (c) 2007-2020 VMware, Inc. or its affiliates. All rights reserved. 6 | %% 7 | 8 | -record(stomp_configuration, {default_login, 9 | default_passcode, 10 | force_default_creds = false, 11 | implicit_connect, 12 | ssl_cert_login}). 13 | 14 | -define(SUPPORTED_VERSIONS, ["1.0", "1.1", "1.2"]). 15 | 16 | -define(INFO_ITEMS, 17 | [conn_name, 18 | connection, 19 | connection_state, 20 | session_id, 21 | channel, 22 | version, 23 | implicit_connect, 24 | auth_login, 25 | auth_mechanism, 26 | peer_addr, 27 | host, 28 | port, 29 | peer_host, 30 | peer_port, 31 | protocol, 32 | channels, 33 | channel_max, 34 | frame_max, 35 | client_properties, 36 | ssl, 37 | ssl_protocol, 38 | ssl_key_exchange, 39 | ssl_cipher, 40 | ssl_hash]). 41 | 42 | -define(STOMP_GUIDE_URL, <<"https://rabbitmq.com/stomp.html">>). 43 | -------------------------------------------------------------------------------- /include/rabbit_stomp_frame.hrl: -------------------------------------------------------------------------------- 1 | %% This Source Code Form is subject to the terms of the Mozilla Public 2 | %% License, v. 2.0. If a copy of the MPL was not distributed with this 3 | %% file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | %% 5 | %% Copyright (c) 2007-2020 VMware, Inc. or its affiliates. All rights reserved. 6 | %% 7 | 8 | -record(stomp_frame, {command, headers, body_iolist}). 9 | -------------------------------------------------------------------------------- /include/rabbit_stomp_headers.hrl: -------------------------------------------------------------------------------- 1 | %% This Source Code Form is subject to the terms of the Mozilla Public 2 | %% License, v. 2.0. If a copy of the MPL was not distributed with this 3 | %% file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | %% 5 | %% Copyright (c) 2007-2020 VMware, Inc. or its affiliates. All rights reserved. 6 | %% 7 | 8 | -define(HEADER_ACCEPT_VERSION, "accept-version"). 9 | -define(HEADER_ACK, "ack"). 10 | -define(HEADER_AMQP_MESSAGE_ID, "amqp-message-id"). 11 | -define(HEADER_APP_ID, "app-id"). 12 | -define(HEADER_AUTO_DELETE, "auto-delete"). 13 | -define(HEADER_CONTENT_ENCODING, "content-encoding"). 14 | -define(HEADER_CONTENT_LENGTH, "content-length"). 15 | -define(HEADER_CONTENT_TYPE, "content-type"). 16 | -define(HEADER_CORRELATION_ID, "correlation-id"). 17 | -define(HEADER_DESTINATION, "destination"). 18 | -define(HEADER_DURABLE, "durable"). 19 | -define(HEADER_EXPIRATION, "expiration"). 20 | -define(HEADER_EXCLUSIVE, "exclusive"). 21 | -define(HEADER_HEART_BEAT, "heart-beat"). 22 | -define(HEADER_HOST, "host"). 23 | -define(HEADER_ID, "id"). 24 | -define(HEADER_LOGIN, "login"). 25 | -define(HEADER_MESSAGE_ID, "message-id"). 26 | -define(HEADER_PASSCODE, "passcode"). 27 | -define(HEADER_PERSISTENT, "persistent"). 28 | -define(HEADER_PREFETCH_COUNT, "prefetch-count"). 29 | -define(HEADER_PRIORITY, "priority"). 30 | -define(HEADER_RECEIPT, "receipt"). 31 | -define(HEADER_REDELIVERED, "redelivered"). 32 | -define(HEADER_REPLY_TO, "reply-to"). 33 | -define(HEADER_SERVER, "server"). 34 | -define(HEADER_SESSION, "session"). 35 | -define(HEADER_SUBSCRIPTION, "subscription"). 36 | -define(HEADER_TIMESTAMP, "timestamp"). 37 | -define(HEADER_TRANSACTION, "transaction"). 38 | -define(HEADER_TYPE, "type"). 39 | -define(HEADER_USER_ID, "user-id"). 40 | -define(HEADER_VERSION, "version"). 41 | -define(HEADER_X_DEAD_LETTER_EXCHANGE, "x-dead-letter-exchange"). 42 | -define(HEADER_X_DEAD_LETTER_ROUTING_KEY, "x-dead-letter-routing-key"). 43 | -define(HEADER_X_EXPIRES, "x-expires"). 44 | -define(HEADER_X_MAX_LENGTH, "x-max-length"). 45 | -define(HEADER_X_MAX_LENGTH_BYTES, "x-max-length-bytes"). 46 | -define(HEADER_X_MAX_PRIORITY, "x-max-priority"). 47 | -define(HEADER_X_MESSAGE_TTL, "x-message-ttl"). 48 | -define(HEADER_X_QUEUE_NAME, "x-queue-name"). 49 | -define(HEADER_X_QUEUE_TYPE, "x-queue-type"). 50 | 51 | -define(MESSAGE_ID_SEPARATOR, "@@"). 52 | 53 | -define(HEADERS_NOT_ON_SEND, [?HEADER_MESSAGE_ID]). 54 | 55 | -define(TEMP_QUEUE_ID_PREFIX, "/temp-queue/"). 56 | 57 | -define(HEADER_ARGUMENTS, [ 58 | ?HEADER_X_DEAD_LETTER_EXCHANGE, 59 | ?HEADER_X_DEAD_LETTER_ROUTING_KEY, 60 | ?HEADER_X_EXPIRES, 61 | ?HEADER_X_MAX_LENGTH, 62 | ?HEADER_X_MAX_LENGTH_BYTES, 63 | ?HEADER_X_MAX_PRIORITY, 64 | ?HEADER_X_MESSAGE_TTL, 65 | ?HEADER_X_QUEUE_TYPE 66 | ]). 67 | 68 | -define(HEADER_PARAMS, [ 69 | ?HEADER_AUTO_DELETE, 70 | ?HEADER_DURABLE, 71 | ?HEADER_EXCLUSIVE, 72 | ?HEADER_PERSISTENT 73 | ]). 74 | -------------------------------------------------------------------------------- /priv/schema/rabbitmq_stomp.schema: -------------------------------------------------------------------------------- 1 | %% This Source Code Form is subject to the terms of the Mozilla Public 2 | %% License, v. 2.0. If a copy of the MPL was not distributed with this 3 | %% file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | %% 5 | %% Copyright (c) 2007-2020 VMware, Inc. or its affiliates. All rights reserved. 6 | %% 7 | 8 | %% ========================================================================== 9 | %% ---------------------------------------------------------------------------- 10 | %% RabbitMQ Stomp Adapter 11 | %% 12 | %% See https://www.rabbitmq.com/stomp.html for details 13 | %% ---------------------------------------------------------------------------- 14 | 15 | % {rabbitmq_stomp, 16 | % [%% Network Configuration - the format is generally the same as for the broker 17 | 18 | %% Listen only on localhost (ipv4 & ipv6) on a specific port. 19 | %% {tcp_listeners, [{"127.0.0.1", 61613}, 20 | %% {"::1", 61613}]}, 21 | 22 | {mapping, "stomp.listeners.tcp", "rabbitmq_stomp.tcp_listeners",[ 23 | {datatype, {enum, [none]}} 24 | ]}. 25 | 26 | {mapping, "stomp.listeners.tcp.$name", "rabbitmq_stomp.tcp_listeners",[ 27 | {datatype, [integer, ip]} 28 | ]}. 29 | 30 | {translation, "rabbitmq_stomp.tcp_listeners", 31 | fun(Conf) -> 32 | case cuttlefish:conf_get("stomp.listeners.tcp", Conf, undefined) of 33 | none -> []; 34 | _ -> 35 | Settings = cuttlefish_variable:filter_by_prefix("stomp.listeners.tcp", Conf), 36 | [ V || {_, V} <- Settings ] 37 | end 38 | end}. 39 | 40 | {mapping, "stomp.tcp_listen_options", "rabbitmq_stomp.tcp_listen_options", [ 41 | {datatype, {enum, [none]}}]}. 42 | 43 | {translation, "rabbitmq_stomp.tcp_listen_options", 44 | fun(Conf) -> 45 | case cuttlefish:conf_get("stomp.tcp_listen_options", Conf, undefined) of 46 | none -> []; 47 | _ -> cuttlefish:invalid("Invalid stomp.tcp_listen_options") 48 | end 49 | end}. 50 | 51 | {mapping, "stomp.tcp_listen_options.backlog", "rabbitmq_stomp.tcp_listen_options.backlog", [ 52 | {datatype, integer} 53 | ]}. 54 | 55 | {mapping, "stomp.tcp_listen_options.nodelay", "rabbitmq_stomp.tcp_listen_options.nodelay", [ 56 | {datatype, {enum, [true, false]}} 57 | ]}. 58 | 59 | {mapping, "stomp.tcp_listen_options.buffer", "rabbitmq_stomp.tcp_listen_options.buffer", 60 | [{datatype, integer}]}. 61 | 62 | {mapping, "stomp.tcp_listen_options.delay_send", "rabbitmq_stomp.tcp_listen_options.delay_send", 63 | [{datatype, {enum, [true, false]}}]}. 64 | 65 | {mapping, "stomp.tcp_listen_options.dontroute", "rabbitmq_stomp.tcp_listen_options.dontroute", 66 | [{datatype, {enum, [true, false]}}]}. 67 | 68 | {mapping, "stomp.tcp_listen_options.exit_on_close", "rabbitmq_stomp.tcp_listen_options.exit_on_close", 69 | [{datatype, {enum, [true, false]}}]}. 70 | 71 | {mapping, "stomp.tcp_listen_options.fd", "rabbitmq_stomp.tcp_listen_options.fd", 72 | [{datatype, integer}]}. 73 | 74 | {mapping, "stomp.tcp_listen_options.high_msgq_watermark", "rabbitmq_stomp.tcp_listen_options.high_msgq_watermark", 75 | [{datatype, integer}]}. 76 | 77 | {mapping, "stomp.tcp_listen_options.high_watermark", "rabbitmq_stomp.tcp_listen_options.high_watermark", 78 | [{datatype, integer}]}. 79 | 80 | {mapping, "stomp.tcp_listen_options.keepalive", "rabbitmq_stomp.tcp_listen_options.keepalive", 81 | [{datatype, {enum, [true, false]}}]}. 82 | 83 | {mapping, "stomp.tcp_listen_options.low_msgq_watermark", "rabbitmq_stomp.tcp_listen_options.low_msgq_watermark", 84 | [{datatype, integer}]}. 85 | 86 | {mapping, "stomp.tcp_listen_options.low_watermark", "rabbitmq_stomp.tcp_listen_options.low_watermark", 87 | [{datatype, integer}]}. 88 | 89 | {mapping, "stomp.tcp_listen_options.port", "rabbitmq_stomp.tcp_listen_options.port", 90 | [{datatype, integer}, {validators, ["port"]}]}. 91 | 92 | {mapping, "stomp.tcp_listen_options.priority", "rabbitmq_stomp.tcp_listen_options.priority", 93 | [{datatype, integer}]}. 94 | 95 | {mapping, "stomp.tcp_listen_options.recbuf", "rabbitmq_stomp.tcp_listen_options.recbuf", 96 | [{datatype, integer}]}. 97 | 98 | {mapping, "stomp.tcp_listen_options.send_timeout", "rabbitmq_stomp.tcp_listen_options.send_timeout", 99 | [{datatype, integer}]}. 100 | 101 | {mapping, "stomp.tcp_listen_options.send_timeout_close", "rabbitmq_stomp.tcp_listen_options.send_timeout_close", 102 | [{datatype, {enum, [true, false]}}]}. 103 | 104 | {mapping, "stomp.tcp_listen_options.sndbuf", "rabbitmq_stomp.tcp_listen_options.sndbuf", 105 | [{datatype, integer}]}. 106 | 107 | {mapping, "stomp.tcp_listen_options.tos", "rabbitmq_stomp.tcp_listen_options.tos", 108 | [{datatype, integer}]}. 109 | 110 | {mapping, "stomp.tcp_listen_options.linger.on", "rabbitmq_stomp.tcp_listen_options.linger", 111 | [{datatype, {enum, [true, false]}}]}. 112 | 113 | {mapping, "stomp.tcp_listen_options.linger.timeout", "rabbitmq_stomp.tcp_listen_options.linger", 114 | [{datatype, integer}, {validators, ["non_negative_integer"]}]}. 115 | 116 | {translation, "rabbitmq_stomp.tcp_listen_options.linger", 117 | fun(Conf) -> 118 | LingerOn = cuttlefish:conf_get("stomp.tcp_listen_options.linger.on", Conf, false), 119 | LingerTimeout = cuttlefish:conf_get("stomp.tcp_listen_options.linger.timeout", Conf, 0), 120 | {LingerOn, LingerTimeout} 121 | end}. 122 | 123 | 124 | %% 125 | %% TLS 126 | %% 127 | 128 | {mapping, "stomp.listeners.ssl", "rabbitmq_stomp.ssl_listeners",[ 129 | {datatype, {enum, [none]}} 130 | ]}. 131 | 132 | {mapping, "stomp.listeners.ssl.$name", "rabbitmq_stomp.ssl_listeners",[ 133 | {datatype, [integer, ip]} 134 | ]}. 135 | 136 | {translation, "rabbitmq_stomp.ssl_listeners", 137 | fun(Conf) -> 138 | case cuttlefish:conf_get("stomp.listeners.ssl", Conf, undefined) of 139 | none -> []; 140 | _ -> 141 | Settings = cuttlefish_variable:filter_by_prefix("stomp.listeners.ssl", Conf), 142 | [ V || {_, V} <- Settings ] 143 | end 144 | end}. 145 | 146 | %% Number of Erlang processes that will accept connections for the TCP 147 | %% and SSL listeners. 148 | %% 149 | %% {num_tcp_acceptors, 10}, 150 | %% {num_ssl_acceptors, 10}, 151 | 152 | {mapping, "stomp.num_acceptors.ssl", "rabbitmq_stomp.num_ssl_acceptors", [ 153 | {datatype, integer} 154 | ]}. 155 | 156 | {mapping, "stomp.num_acceptors.tcp", "rabbitmq_stomp.num_tcp_acceptors", [ 157 | {datatype, integer} 158 | ]}. 159 | 160 | %% Additional TLS options 161 | 162 | %% Extract a name from the client's certificate when using TLS. 163 | %% 164 | %% Defaults to true. 165 | 166 | {mapping, "stomp.ssl_cert_login", "rabbitmq_stomp.ssl_cert_login", 167 | [{datatype, {enum, [true, false]}}]}. 168 | 169 | %% Set a default user name and password. This is used as the default login 170 | %% whenever a CONNECT frame omits the login and passcode headers. 171 | %% 172 | %% Please note that setting this will allow clients to connect without 173 | %% authenticating! 174 | %% 175 | %% {default_user, [{login, "guest"}, 176 | %% {passcode, "guest"}]}, 177 | 178 | {mapping, "stomp.default_vhost", "rabbitmq_stomp.default_vhost", [ 179 | {datatype, string} 180 | ]}. 181 | 182 | {translation, "rabbitmq_stomp.default_vhost", 183 | fun(Conf) -> 184 | list_to_binary(cuttlefish:conf_get("stomp.default_vhost", Conf, "/")) 185 | end}. 186 | 187 | {mapping, "stomp.default_user", "rabbitmq_stomp.default_user.login", [ 188 | {datatype, string} 189 | ]}. 190 | 191 | {mapping, "stomp.default_pass", "rabbitmq_stomp.default_user.passcode", [ 192 | {datatype, string} 193 | ]}. 194 | 195 | {mapping, "stomp.default_topic_exchange", "rabbitmq_stomp.default_topic_exchange", [ 196 | {datatype, string} 197 | ]}. 198 | 199 | {translation, "rabbitmq_stomp.default_topic_exchange", 200 | fun(Conf) -> 201 | list_to_binary(cuttlefish:conf_get("stomp.default_topic_exchange", Conf, "amq.topic")) 202 | end}. 203 | 204 | %% If a default user is configured, or if x.509 205 | %% certificate-based client authentication is used, use this setting to allow clients to 206 | %% omit the CONNECT frame entirely. If set to true, the client is 207 | %% automatically connected as the default user or user supplied in the 208 | %% x.509/TLS certificate whenever the first frame sent on a session is not a 209 | %% CONNECT frame. 210 | %% 211 | %% Defaults to true. 212 | 213 | {mapping, "stomp.implicit_connect", "rabbitmq_stomp.implicit_connect", 214 | [{datatype, {enum, [true, false]}}]}. 215 | 216 | %% Whether or not to enable proxy protocol support. 217 | %% 218 | %% Defaults to false. 219 | 220 | {mapping, "stomp.proxy_protocol", "rabbitmq_stomp.proxy_protocol", 221 | [{datatype, {enum, [true, false]}}]}. 222 | 223 | %% Whether or not to hide server info 224 | %% 225 | %% Defaults to false. 226 | 227 | {mapping, "stomp.hide_server_info", "rabbitmq_stomp.hide_server_info", 228 | [{datatype, {enum, [true, false]}}]}. 229 | 230 | %% Whether or not to always requeue the message on nack 231 | %% If not set then coordinated by the usage of the frame "requeue" header 232 | %% Useful when you are not fully controlling the STOMP consumer implementation 233 | %% 234 | %% Defaults to true. 235 | 236 | {mapping, "stomp.default_nack_requeue", "rabbitmq_stomp.default_nack_requeue", 237 | [{datatype, {enum, [true, false]}}]}. 238 | -------------------------------------------------------------------------------- /src/Elixir.RabbitMQ.CLI.Ctl.Commands.ListStompConnectionsCommand.erl: -------------------------------------------------------------------------------- 1 | %% The contents of this file are subject to the Mozilla Public License 2 | %% Version 1.1 (the "License"); you may not use this file except in 3 | %% compliance with the License. You may obtain a copy of the License 4 | %% at https://www.mozilla.org/MPL/ 5 | %% 6 | %% Software distributed under the License is distributed on an "AS IS" 7 | %% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See 8 | %% the License for the specific language governing rights and 9 | %% limitations under the License. 10 | %% 11 | %% The Original Code is RabbitMQ. 12 | %% 13 | %% The Initial Developer of the Original Code is GoPivotal, Inc. 14 | %% Copyright (c) 2007-2020 VMware, Inc. or its affiliates. All rights reserved. 15 | 16 | -module('Elixir.RabbitMQ.CLI.Ctl.Commands.ListStompConnectionsCommand'). 17 | 18 | -include("rabbit_stomp.hrl"). 19 | 20 | -behaviour('Elixir.RabbitMQ.CLI.CommandBehaviour'). 21 | 22 | -export([formatter/0, 23 | scopes/0, 24 | switches/0, 25 | aliases/0, 26 | usage/0, 27 | usage_additional/0, 28 | usage_doc_guides/0, 29 | banner/2, 30 | validate/2, 31 | merge_defaults/2, 32 | run/2, 33 | output/2, 34 | description/0, 35 | help_section/0]). 36 | 37 | formatter() -> 'Elixir.RabbitMQ.CLI.Formatters.Table'. 38 | 39 | scopes() -> [ctl, diagnostics]. 40 | 41 | switches() -> [{verbose, boolean}]. 42 | aliases() -> [{'V', verbose}]. 43 | 44 | description() -> <<"Lists STOMP connections on the target node">>. 45 | 46 | help_section() -> 47 | {plugin, stomp}. 48 | 49 | validate(Args, _) -> 50 | case 'Elixir.RabbitMQ.CLI.Ctl.InfoKeys':validate_info_keys(Args, 51 | ?INFO_ITEMS) of 52 | {ok, _} -> ok; 53 | Error -> Error 54 | end. 55 | 56 | merge_defaults([], Opts) -> 57 | merge_defaults([<<"session_id">>, <<"conn_name">>], Opts); 58 | merge_defaults(Args, Opts) -> 59 | {Args, maps:merge(#{verbose => false}, Opts)}. 60 | 61 | usage() -> 62 | <<"list_stomp_connections [ ...]">>. 63 | 64 | usage_additional() -> 65 | Prefix = <<" must be one of ">>, 66 | InfoItems = 'Elixir.Enum':join(lists:usort(?INFO_ITEMS), <<", ">>), 67 | [ 68 | {<<"">>, <>} 69 | ]. 70 | 71 | usage_doc_guides() -> 72 | [?STOMP_GUIDE_URL]. 73 | 74 | run(Args, #{node := NodeName, 75 | timeout := Timeout, 76 | verbose := Verbose}) -> 77 | InfoKeys = case Verbose of 78 | true -> ?INFO_ITEMS; 79 | false -> 'Elixir.RabbitMQ.CLI.Ctl.InfoKeys':prepare_info_keys(Args) 80 | end, 81 | Nodes = 'Elixir.RabbitMQ.CLI.Core.Helpers':nodes_in_cluster(NodeName), 82 | 83 | 'Elixir.RabbitMQ.CLI.Ctl.RpcStream':receive_list_items( 84 | NodeName, 85 | rabbit_stomp, 86 | emit_connection_info_all, 87 | [Nodes, InfoKeys], 88 | Timeout, 89 | InfoKeys, 90 | length(Nodes)). 91 | 92 | banner(_, _) -> <<"Listing STOMP connections ...">>. 93 | 94 | output(Result, _Opts) -> 95 | 'Elixir.RabbitMQ.CLI.DefaultOutput':output(Result). 96 | -------------------------------------------------------------------------------- /src/rabbit_stomp.erl: -------------------------------------------------------------------------------- 1 | %% The contents of this file are subject to the Mozilla Public License 2 | %% Version 1.1 (the "License"); you may not use this file except in 3 | %% compliance with the License. You may obtain a copy of the License 4 | %% at https://www.mozilla.org/MPL/ 5 | %% 6 | %% Software distributed under the License is distributed on an "AS IS" 7 | %% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See 8 | %% the License for the specific language governing rights and 9 | %% limitations under the License. 10 | %% 11 | %% The Original Code is RabbitMQ. 12 | %% 13 | %% The Initial Developer of the Original Code is GoPivotal, Inc. 14 | %% Copyright (c) 2007-2020 VMware, Inc. or its affiliates. All rights reserved. 15 | %% 16 | 17 | -module(rabbit_stomp). 18 | 19 | -include("rabbit_stomp.hrl"). 20 | 21 | -behaviour(application). 22 | -export([start/2, stop/1]). 23 | -export([parse_default_user/2]). 24 | -export([connection_info_local/1, 25 | emit_connection_info_local/3, 26 | emit_connection_info_all/4, 27 | list/0, 28 | close_all_client_connections/1]). 29 | 30 | -define(DEFAULT_CONFIGURATION, 31 | #stomp_configuration{ 32 | default_login = undefined, 33 | default_passcode = undefined, 34 | implicit_connect = false, 35 | ssl_cert_login = false}). 36 | 37 | start(normal, []) -> 38 | Config = parse_configuration(), 39 | Listeners = parse_listener_configuration(), 40 | Result = rabbit_stomp_sup:start_link(Listeners, Config), 41 | EMPid = case rabbit_event:start_link() of 42 | {ok, Pid} -> Pid; 43 | {error, {already_started, Pid}} -> Pid 44 | end, 45 | gen_event:add_handler(EMPid, rabbit_stomp_internal_event_handler, []), 46 | Result. 47 | 48 | stop(_) -> 49 | rabbit_stomp_sup:stop_listeners(). 50 | 51 | -spec close_all_client_connections(string() | binary()) -> {'ok', non_neg_integer()}. 52 | close_all_client_connections(Reason) -> 53 | Connections = list(), 54 | [rabbit_stomp_reader:close_connection(Pid, Reason) || Pid <- Connections], 55 | {ok, length(Connections)}. 56 | 57 | emit_connection_info_all(Nodes, Items, Ref, AggregatorPid) -> 58 | Pids = [spawn_link(Node, rabbit_stomp, emit_connection_info_local, 59 | [Items, Ref, AggregatorPid]) 60 | || Node <- Nodes], 61 | rabbit_control_misc:await_emitters_termination(Pids), 62 | ok. 63 | 64 | emit_connection_info_local(Items, Ref, AggregatorPid) -> 65 | rabbit_control_misc:emitting_map_with_exit_handler( 66 | AggregatorPid, Ref, fun(Pid) -> 67 | rabbit_stomp_reader:info(Pid, Items) 68 | end, 69 | list()). 70 | 71 | connection_info_local(Items) -> 72 | Connections = list(), 73 | [rabbit_stomp_reader:info(Pid, Items) || Pid <- Connections]. 74 | 75 | parse_listener_configuration() -> 76 | {ok, Listeners} = application:get_env(tcp_listeners), 77 | {ok, SslListeners} = application:get_env(ssl_listeners), 78 | {Listeners, SslListeners}. 79 | 80 | parse_configuration() -> 81 | {ok, UserConfig} = application:get_env(default_user), 82 | Conf0 = parse_default_user(UserConfig, ?DEFAULT_CONFIGURATION), 83 | {ok, SSLLogin} = application:get_env(ssl_cert_login), 84 | {ok, ImplicitConnect} = application:get_env(implicit_connect), 85 | Conf = Conf0#stomp_configuration{ssl_cert_login = SSLLogin, 86 | implicit_connect = ImplicitConnect}, 87 | report_configuration(Conf), 88 | Conf. 89 | 90 | parse_default_user([], Configuration) -> 91 | Configuration; 92 | parse_default_user([{login, Login} | Rest], Configuration) -> 93 | parse_default_user(Rest, Configuration#stomp_configuration{ 94 | default_login = Login}); 95 | parse_default_user([{passcode, Passcode} | Rest], Configuration) -> 96 | parse_default_user(Rest, Configuration#stomp_configuration{ 97 | default_passcode = Passcode}); 98 | parse_default_user([Unknown | Rest], Configuration) -> 99 | rabbit_log:warning("rabbit_stomp: ignoring invalid default_user " 100 | "configuration option: ~p~n", [Unknown]), 101 | parse_default_user(Rest, Configuration). 102 | 103 | report_configuration(#stomp_configuration{ 104 | default_login = Login, 105 | implicit_connect = ImplicitConnect, 106 | ssl_cert_login = SSLCertLogin}) -> 107 | case Login of 108 | undefined -> ok; 109 | _ -> rabbit_log:info("rabbit_stomp: default user '~s' " 110 | "enabled~n", [Login]) 111 | end, 112 | 113 | case ImplicitConnect of 114 | true -> rabbit_log:info("rabbit_stomp: implicit connect enabled~n"); 115 | false -> ok 116 | end, 117 | 118 | case SSLCertLogin of 119 | true -> rabbit_log:info("rabbit_stomp: ssl_cert_login enabled~n"); 120 | false -> ok 121 | end, 122 | 123 | ok. 124 | 125 | list() -> 126 | [Client 127 | || {_, ListSupPid, _, _} <- supervisor2:which_children(rabbit_stomp_sup), 128 | {_, RanchSup, supervisor, _} <- supervisor2:which_children(ListSupPid), 129 | {ranch_conns_sup, ConnSup, _, _} <- supervisor:which_children(RanchSup), 130 | {_, CliSup, _, _} <- supervisor:which_children(ConnSup), 131 | {rabbit_stomp_reader, Client, _, _} <- supervisor:which_children(CliSup)]. 132 | -------------------------------------------------------------------------------- /src/rabbit_stomp_client_sup.erl: -------------------------------------------------------------------------------- 1 | %% The contents of this file are subject to the Mozilla Public License 2 | %% Version 1.1 (the "License"); you may not use this file except in 3 | %% compliance with the License. You may obtain a copy of the License 4 | %% at https://www.mozilla.org/MPL/ 5 | %% 6 | %% Software distributed under the License is distributed on an "AS IS" 7 | %% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See 8 | %% the License for the specific language governing rights and 9 | %% limitations under the License. 10 | %% 11 | %% The Original Code is RabbitMQ. 12 | %% 13 | %% The Initial Developer of the Original Code is GoPivotal, Inc. 14 | %% Copyright (c) 2007-2020 VMware, Inc. or its affiliates. All rights reserved. 15 | %% 16 | 17 | -module(rabbit_stomp_client_sup). 18 | -behaviour(supervisor2). 19 | -behaviour(ranch_protocol). 20 | 21 | -include_lib("rabbit_common/include/rabbit.hrl"). 22 | 23 | -export([start_link/4, init/1]). 24 | 25 | start_link(Ref, _Sock, _Transport, Configuration) -> 26 | {ok, SupPid} = supervisor2:start_link(?MODULE, []), 27 | {ok, HelperPid} = 28 | supervisor2:start_child(SupPid, 29 | {rabbit_stomp_heartbeat_sup, 30 | {rabbit_connection_helper_sup, start_link, []}, 31 | intrinsic, infinity, supervisor, 32 | [rabbit_connection_helper_sup]}), 33 | 34 | %% We want the reader to be transient since when it exits normally 35 | %% the processor may have some work still to do (and the reader 36 | %% tells the processor to exit). However, if the reader terminates 37 | %% abnormally then we want to take everything down. 38 | {ok, ReaderPid} = supervisor2:start_child( 39 | SupPid, 40 | {rabbit_stomp_reader, 41 | {rabbit_stomp_reader, 42 | start_link, [HelperPid, Ref, Configuration]}, 43 | intrinsic, ?WORKER_WAIT, worker, 44 | [rabbit_stomp_reader]}), 45 | 46 | {ok, SupPid, ReaderPid}. 47 | 48 | init([]) -> 49 | {ok, {{one_for_all, 0, 1}, []}}. 50 | 51 | -------------------------------------------------------------------------------- /src/rabbit_stomp_connection_info.erl: -------------------------------------------------------------------------------- 1 | %% The contents of this file are subject to the Mozilla Public License 2 | %% Version 1.1 (the "License"); you may not use this file except in 3 | %% compliance with the License. You may obtain a copy of the License 4 | %% at https://www.mozilla.org/MPL/ 5 | %% 6 | %% Software distributed under the License is distributed on an "AS IS" 7 | %% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See 8 | %% the License for the specific language governing rights and 9 | %% limitations under the License. 10 | %% 11 | %% The Original Code is RabbitMQ. 12 | %% 13 | %% The Initial Developer of the Original Code is GoPivotal, Inc. 14 | %% Copyright (c) 2018-2020 VMware, Inc. or its affiliates. All rights reserved. 15 | %% 16 | -module(rabbit_stomp_connection_info). 17 | 18 | %% Note: this is necessary to prevent code:get_object_code from 19 | %% backing up due to a missing module. See VESC-888. 20 | 21 | %% API 22 | -export([additional_authn_params/4]). 23 | 24 | additional_authn_params(_Creds, _VHost, _Pid, _Infos) -> 25 | []. 26 | -------------------------------------------------------------------------------- /src/rabbit_stomp_frame.erl: -------------------------------------------------------------------------------- 1 | %% The contents of this file are subject to the Mozilla Public License 2 | %% Version 1.1 (the "License"); you may not use this file except in 3 | %% compliance with the License. You may obtain a copy of the License 4 | %% at https://www.mozilla.org/MPL/ 5 | %% 6 | %% Software distributed under the License is distributed on an "AS IS" 7 | %% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See 8 | %% the License for the specific language governing rights and 9 | %% limitations under the License. 10 | %% 11 | %% The Original Code is RabbitMQ. 12 | %% 13 | %% The Initial Developer of the Original Code is GoPivotal, Inc. 14 | %% Copyright (c) 2007-2020 VMware, Inc. or its affiliates. All rights reserved. 15 | %% 16 | 17 | %% stomp_frame implements the STOMP framing protocol "version 1.0", as 18 | %% per https://stomp.codehaus.org/Protocol 19 | 20 | -module(rabbit_stomp_frame). 21 | 22 | -include("rabbit_stomp_frame.hrl"). 23 | -include("rabbit_stomp_headers.hrl"). 24 | 25 | -export([parse/2, initial_state/0]). 26 | -export([header/2, header/3, 27 | boolean_header/2, boolean_header/3, 28 | integer_header/2, integer_header/3, 29 | binary_header/2, binary_header/3]). 30 | -export([serialize/1, serialize/2]). 31 | 32 | initial_state() -> none. 33 | 34 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 35 | %% STOMP 1.1 frames basic syntax 36 | %% Rabbit modifications: 37 | %% o CR LF is equivalent to LF in all element terminators (eol). 38 | %% o Escape codes for header names and values include \r for CR 39 | %% and CR is not allowed. 40 | %% o Header names and values are not limited to UTF-8 strings. 41 | %% o Header values may contain unescaped colons 42 | %% 43 | %% frame_seq ::= *(noise frame) 44 | %% noise ::= *(NUL | eol) 45 | %% eol ::= LF | CR LF 46 | %% frame ::= cmd hdrs body NUL 47 | %% body ::= *OCTET 48 | %% cmd ::= 1*NOTEOL eol 49 | %% hdrs ::= *hdr eol 50 | %% hdr ::= hdrname COLON hdrvalue eol 51 | %% hdrname ::= 1*esc_char 52 | %% hdrvalue ::= *esc_char 53 | %% esc_char ::= HDROCT | BACKSLASH ESCCODE 54 | %% 55 | %% Terms in CAPS all represent sets (alternatives) of single octets. 56 | %% They are defined here using a small extension of BNF, minus (-): 57 | %% 58 | %% term1 - term2 denotes any of the possibilities in term1 59 | %% excluding those in term2. 60 | %% In this grammar minus is only used for sets of single octets. 61 | %% 62 | %% OCTET ::= '00'x..'FF'x % any octet 63 | %% NUL ::= '00'x % the zero octet 64 | %% LF ::= '\n' % '0a'x newline or linefeed 65 | %% CR ::= '\r' % '0d'x carriage return 66 | %% NOTEOL ::= OCTET - (CR | LF) % any octet except CR or LF 67 | %% BACKSLASH ::= '\\' % '5c'x 68 | %% ESCCODE ::= 'c' | 'n' | 'r' | BACKSLASH 69 | %% COLON ::= ':' 70 | %% HDROCT ::= NOTEOL - (COLON | BACKSLASH) 71 | %% % octets allowed in a header 72 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 73 | 74 | %% explicit frame characters 75 | -define(NUL, 0). 76 | -define(CR, $\r). 77 | -define(LF, $\n). 78 | -define(BSL, $\\). 79 | -define(COLON, $:). 80 | 81 | %% header escape codes 82 | -define(LF_ESC, $n). 83 | -define(BSL_ESC, $\\). 84 | -define(COLON_ESC, $c). 85 | -define(CR_ESC, $r). 86 | 87 | %% parser state 88 | -record(state, {acc, cmd, hdrs, hdrname}). 89 | 90 | parse(Content, {resume, Continuation}) -> Continuation(Content); 91 | parse(Content, none ) -> parser(Content, noframe, #state{}). 92 | 93 | more(Continuation) -> {more, {resume, Continuation}}. 94 | 95 | %% Single-function parser: Term :: noframe | command | headers | hdrname | hdrvalue 96 | %% general more and line-end detection 97 | parser(<<>>, Term , State) -> more(fun(Rest) -> parser(Rest, Term, State) end); 98 | parser(<>, Term , State) -> more(fun(Rest) -> parser(<>, Term, State) end); 99 | parser(<>, Term , State) -> parser(<>, Term, State); 100 | parser(<>, Term , _State) -> {error, {unexpected_chars(Term), [?CR, Ch]}}; 101 | %% escape processing (only in hdrname and hdrvalue terms) 102 | parser(<>, Term , State) -> more(fun(Rest) -> parser(<>, Term, State) end); 103 | parser(<>, Term , State) 104 | when Term == hdrname; 105 | Term == hdrvalue -> unescape(Ch, fun(Ech) -> parser(Rest, Term, accum(Ech, State)) end); 106 | %% inter-frame noise 107 | parser(<>, noframe , State) -> parser(Rest, noframe, State); 108 | parser(<>, noframe , State) -> parser(Rest, noframe, State); 109 | %% detect transitions 110 | parser( Rest, noframe , State) -> goto(noframe, command, Rest, State); 111 | parser(<>, command , State) -> goto(command, headers, Rest, State); 112 | parser(<>, headers , State) -> goto(headers, body, Rest, State); 113 | parser( Rest, headers , State) -> goto(headers, hdrname, Rest, State); 114 | parser(<>, hdrname , State) -> goto(hdrname, hdrvalue, Rest, State); 115 | parser(<>, hdrname , State) -> goto(hdrname, headers, Rest, State); 116 | parser(<>, hdrvalue, State) -> goto(hdrvalue, headers, Rest, State); 117 | %% accumulate 118 | parser(<>, Term , State) -> parser(Rest, Term, accum(Ch, State)). 119 | 120 | %% state transitions 121 | goto(noframe, command, Rest, State ) -> parser(Rest, command, State#state{acc = []}); 122 | goto(command, headers, Rest, State = #state{acc = Acc} ) -> parser(Rest, headers, State#state{cmd = lists:reverse(Acc), hdrs = []}); 123 | goto(headers, body, Rest, #state{cmd = Cmd, hdrs = Hdrs}) -> parse_body(Rest, #stomp_frame{command = Cmd, headers = Hdrs}); 124 | goto(headers, hdrname, Rest, State ) -> parser(Rest, hdrname, State#state{acc = []}); 125 | goto(hdrname, hdrvalue, Rest, State = #state{acc = Acc} ) -> parser(Rest, hdrvalue, State#state{acc = [], hdrname = lists:reverse(Acc)}); 126 | goto(hdrname, headers, _Rest, #state{acc = Acc} ) -> {error, {header_no_value, lists:reverse(Acc)}}; % badly formed header -- fatal error 127 | goto(hdrvalue, headers, Rest, State = #state{acc = Acc, hdrs = Headers, hdrname = HdrName}) -> 128 | parser(Rest, headers, State#state{hdrs = insert_header(Headers, HdrName, lists:reverse(Acc))}). 129 | 130 | %% error atom 131 | unexpected_chars(noframe) -> unexpected_chars_between_frames; 132 | unexpected_chars(command) -> unexpected_chars_in_command; 133 | unexpected_chars(hdrname) -> unexpected_chars_in_header; 134 | unexpected_chars(hdrvalue) -> unexpected_chars_in_header; 135 | unexpected_chars(_Term) -> unexpected_chars. 136 | 137 | %% general accumulation 138 | accum(Ch, State = #state{acc = Acc}) -> State#state{acc = [Ch | Acc]}. 139 | 140 | %% resolve escapes (with error processing) 141 | unescape(?LF_ESC, Fun) -> Fun(?LF); 142 | unescape(?BSL_ESC, Fun) -> Fun(?BSL); 143 | unescape(?COLON_ESC, Fun) -> Fun(?COLON); 144 | unescape(?CR_ESC, Fun) -> Fun(?CR); 145 | unescape(Ch, _Fun) -> {error, {bad_escape, [?BSL, Ch]}}. 146 | 147 | %% insert header unless aleady seen 148 | insert_header(Headers, Name, Value) -> 149 | case lists:keymember(Name, 1, Headers) of 150 | true -> Headers; % first header only 151 | false -> [{Name, Value} | Headers] 152 | end. 153 | 154 | parse_body(Content, Frame = #stomp_frame{command = Command}) -> 155 | case Command of 156 | "SEND" -> parse_body(Content, Frame, [], integer_header(Frame, ?HEADER_CONTENT_LENGTH, unknown)); 157 | _ -> parse_body(Content, Frame, [], unknown) 158 | end. 159 | 160 | parse_body(Content, Frame, Chunks, unknown) -> 161 | parse_body2(Content, Frame, Chunks, case firstnull(Content) of 162 | -1 -> {more, unknown}; 163 | Pos -> {done, Pos} 164 | end); 165 | parse_body(Content, Frame, Chunks, Remaining) -> 166 | Size = byte_size(Content), 167 | parse_body2(Content, Frame, Chunks, case Remaining >= Size of 168 | true -> {more, Remaining - Size}; 169 | false -> {done, Remaining} 170 | end). 171 | 172 | parse_body2(Content, Frame, Chunks, {more, Left}) -> 173 | Chunks1 = finalize_chunk(Content, Chunks), 174 | more(fun(Rest) -> parse_body(Rest, Frame, Chunks1, Left) end); 175 | parse_body2(Content, Frame, Chunks, {done, Pos}) -> 176 | <> = Content, 177 | Body = lists:reverse(finalize_chunk(Chunk, Chunks)), 178 | {ok, Frame#stomp_frame{body_iolist = Body}, Rest}. 179 | 180 | finalize_chunk(<<>>, Chunks) -> Chunks; 181 | finalize_chunk(Chunk, Chunks) -> [Chunk | Chunks]. 182 | 183 | default_value({ok, Value}, _DefaultValue) -> Value; 184 | default_value(not_found, DefaultValue) -> DefaultValue. 185 | 186 | header(#stomp_frame{headers = Headers}, Key) -> 187 | case lists:keysearch(Key, 1, Headers) of 188 | {value, {_, Str}} -> {ok, Str}; 189 | _ -> not_found 190 | end. 191 | 192 | header(F, K, D) -> default_value(header(F, K), D). 193 | 194 | boolean_header(#stomp_frame{headers = Headers}, Key) -> 195 | case lists:keysearch(Key, 1, Headers) of 196 | {value, {_, "true"}} -> {ok, true}; 197 | {value, {_, "false"}} -> {ok, false}; 198 | %% some Python clients serialize True/False as "True"/"False" 199 | {value, {_, "True"}} -> {ok, true}; 200 | {value, {_, "False"}} -> {ok, false}; 201 | _ -> not_found 202 | end. 203 | 204 | boolean_header(F, K, D) -> default_value(boolean_header(F, K), D). 205 | 206 | internal_integer_header(Headers, Key) -> 207 | case lists:keysearch(Key, 1, Headers) of 208 | {value, {_, Str}} -> {ok, list_to_integer(string:strip(Str))}; 209 | _ -> not_found 210 | end. 211 | 212 | integer_header(#stomp_frame{headers = Headers}, Key) -> 213 | internal_integer_header(Headers, Key). 214 | 215 | integer_header(F, K, D) -> default_value(integer_header(F, K), D). 216 | 217 | binary_header(F, K) -> 218 | case header(F, K) of 219 | {ok, Str} -> {ok, list_to_binary(Str)}; 220 | not_found -> not_found 221 | end. 222 | 223 | binary_header(F, K, D) -> default_value(binary_header(F, K), D). 224 | 225 | serialize(Frame) -> 226 | serialize(Frame, true). 227 | 228 | %% second argument controls whether a trailing linefeed 229 | %% character should be added, see rabbitmq/rabbitmq-stomp#39. 230 | serialize(Frame, true) -> 231 | serialize(Frame, false) ++ [?LF]; 232 | serialize(#stomp_frame{command = Command, 233 | headers = Headers, 234 | body_iolist = BodyFragments}, false) -> 235 | Len = iolist_size(BodyFragments), 236 | [Command, ?LF, 237 | lists:map(fun serialize_header/1, 238 | lists:keydelete(?HEADER_CONTENT_LENGTH, 1, Headers)), 239 | if 240 | Len > 0 -> [?HEADER_CONTENT_LENGTH ++ ":", integer_to_list(Len), ?LF]; 241 | true -> [] 242 | end, 243 | ?LF, BodyFragments, 0]. 244 | 245 | serialize_header({K, V}) when is_integer(V) -> hdr(escape(K), integer_to_list(V)); 246 | serialize_header({K, V}) when is_boolean(V) -> hdr(escape(K), boolean_to_list(V)); 247 | serialize_header({K, V}) when is_list(V) -> hdr(escape(K), escape(V)). 248 | 249 | boolean_to_list(true) -> "true"; 250 | boolean_to_list(_) -> "false". 251 | 252 | hdr(K, V) -> [K, ?COLON, V, ?LF]. 253 | 254 | escape(Str) -> [escape1(Ch) || Ch <- Str]. 255 | 256 | escape1(?COLON) -> [?BSL, ?COLON_ESC]; 257 | escape1(?BSL) -> [?BSL, ?BSL_ESC]; 258 | escape1(?LF) -> [?BSL, ?LF_ESC]; 259 | escape1(?CR) -> [?BSL, ?CR_ESC]; 260 | escape1(Ch) -> Ch. 261 | 262 | firstnull(Content) -> firstnull(Content, 0). 263 | 264 | firstnull(<<>>, _N) -> -1; 265 | firstnull(<<0, _Rest/binary>>, N) -> N; 266 | firstnull(<<_Ch, Rest/binary>>, N) -> firstnull(Rest, N+1). 267 | -------------------------------------------------------------------------------- /src/rabbit_stomp_internal_event_handler.erl: -------------------------------------------------------------------------------- 1 | %% The contents of this file are subject to the Mozilla Public License 2 | %% Version 1.1 (the "License"); you may not use this file except in 3 | %% compliance with the License. You may obtain a copy of the License 4 | %% at https://www.mozilla.org/MPL/ 5 | %% 6 | %% Software distributed under the License is distributed on an "AS IS" 7 | %% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See 8 | %% the License for the specific language governing rights and 9 | %% limitations under the License. 10 | %% 11 | %% The Original Code is RabbitMQ. 12 | %% 13 | %% The Initial Developer of the Original Code is GoPivotal, Inc. 14 | %% Copyright (c) 2007-2020 VMware, Inc. or its affiliates. All rights reserved. 15 | %% 16 | 17 | -module(rabbit_stomp_internal_event_handler). 18 | 19 | -behaviour(gen_event). 20 | 21 | -export([init/1, handle_event/2, handle_call/2, handle_info/2, terminate/2, code_change/3]). 22 | 23 | -import(rabbit_misc, [pget/2]). 24 | 25 | init([]) -> 26 | {ok, []}. 27 | 28 | handle_event({event, maintenance_connections_closed, _Info, _, _}, State) -> 29 | %% we should close our connections 30 | {ok, NConnections} = rabbit_stomp:close_all_client_connections("node is being put into maintenance mode"), 31 | rabbit_log:alert("Closed ~b local STOMP client connections", [NConnections]), 32 | {ok, State}; 33 | handle_event(_Event, State) -> 34 | {ok, State}. 35 | 36 | handle_call(_Request, State) -> 37 | {ok, State}. 38 | 39 | handle_info(_Info, State) -> 40 | {ok, State}. 41 | 42 | terminate(_Reason, _State) -> 43 | ok. 44 | 45 | code_change(_OldVsn, State, _Extra) -> 46 | {ok, State}. 47 | -------------------------------------------------------------------------------- /src/rabbit_stomp_sup.erl: -------------------------------------------------------------------------------- 1 | %% The contents of this file are subject to the Mozilla Public License 2 | %% Version 1.1 (the "License"); you may not use this file except in 3 | %% compliance with the License. You may obtain a copy of the License 4 | %% at https://www.mozilla.org/MPL/ 5 | %% 6 | %% Software distributed under the License is distributed on an "AS IS" 7 | %% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See 8 | %% the License for the specific language governing rights and 9 | %% limitations under the License. 10 | %% 11 | %% The Original Code is RabbitMQ. 12 | %% 13 | %% The Initial Developer of the Original Code is GoPivotal, Inc. 14 | %% Copyright (c) 2007-2020 VMware, Inc. or its affiliates. All rights reserved. 15 | %% 16 | 17 | -module(rabbit_stomp_sup). 18 | -behaviour(supervisor). 19 | 20 | -export([start_link/2, init/1, stop_listeners/0]). 21 | 22 | -define(TCP_PROTOCOL, 'stomp'). 23 | -define(TLS_PROTOCOL, 'stomp/ssl'). 24 | 25 | start_link(Listeners, Configuration) -> 26 | supervisor:start_link({local, ?MODULE}, ?MODULE, 27 | [Listeners, Configuration]). 28 | 29 | init([{Listeners, SslListeners0}, Configuration]) -> 30 | NumTcpAcceptors = application:get_env(rabbitmq_stomp, num_tcp_acceptors, 10), 31 | {ok, SocketOpts} = application:get_env(rabbitmq_stomp, tcp_listen_options), 32 | {SslOpts, NumSslAcceptors, SslListeners} 33 | = case SslListeners0 of 34 | [] -> {none, 0, []}; 35 | _ -> {rabbit_networking:ensure_ssl(), 36 | application:get_env(rabbitmq_stomp, num_ssl_acceptors, 10), 37 | case rabbit_networking:poodle_check('STOMP') of 38 | ok -> SslListeners0; 39 | danger -> [] 40 | end} 41 | end, 42 | Flags = #{ 43 | strategy => one_for_all, 44 | period => 10, 45 | intensity => 10 46 | }, 47 | {ok, {Flags, 48 | listener_specs(fun tcp_listener_spec/1, 49 | [SocketOpts, Configuration, NumTcpAcceptors], Listeners) ++ 50 | listener_specs(fun ssl_listener_spec/1, 51 | [SocketOpts, SslOpts, Configuration, NumSslAcceptors], SslListeners)}}. 52 | 53 | stop_listeners() -> 54 | rabbit_networking:stop_ranch_listener_of_protocol(?TCP_PROTOCOL), 55 | rabbit_networking:stop_ranch_listener_of_protocol(?TLS_PROTOCOL), 56 | ok. 57 | 58 | %% 59 | %% Implementation 60 | %% 61 | 62 | listener_specs(Fun, Args, Listeners) -> 63 | [Fun([Address | Args]) || 64 | Listener <- Listeners, 65 | Address <- rabbit_networking:tcp_listener_addresses(Listener)]. 66 | 67 | tcp_listener_spec([Address, SocketOpts, Configuration, NumAcceptors]) -> 68 | rabbit_networking:tcp_listener_spec( 69 | rabbit_stomp_listener_sup, Address, SocketOpts, 70 | transport(?TCP_PROTOCOL), rabbit_stomp_client_sup, Configuration, 71 | stomp, NumAcceptors, "STOMP TCP listener"). 72 | 73 | ssl_listener_spec([Address, SocketOpts, SslOpts, Configuration, NumAcceptors]) -> 74 | rabbit_networking:tcp_listener_spec( 75 | rabbit_stomp_listener_sup, Address, SocketOpts ++ SslOpts, 76 | transport(?TLS_PROTOCOL), rabbit_stomp_client_sup, Configuration, 77 | 'stomp/ssl', NumAcceptors, "STOMP TLS listener"). 78 | 79 | transport(Protocol) -> 80 | case Protocol of 81 | ?TCP_PROTOCOL -> ranch_tcp; 82 | ?TLS_PROTOCOL -> ranch_ssl 83 | end. 84 | -------------------------------------------------------------------------------- /test/amqqueue_SUITE.erl: -------------------------------------------------------------------------------- 1 | %% This Source Code Form is subject to the terms of the Mozilla Public 2 | %% License, v. 2.0. If a copy of the MPL was not distributed with this 3 | %% file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | %% 5 | %% Copyright (c) 2007-2020 VMware, Inc. or its affiliates. All rights reserved. 6 | %% 7 | 8 | -module(amqqueue_SUITE). 9 | 10 | -compile(export_all). 11 | 12 | -include_lib("common_test/include/ct.hrl"). 13 | -include_lib("eunit/include/eunit.hrl"). 14 | -include_lib("amqp_client/include/amqp_client.hrl"). 15 | -include("rabbit_stomp.hrl"). 16 | -include("rabbit_stomp_frame.hrl"). 17 | -include("rabbit_stomp_headers.hrl"). 18 | 19 | -define(QUEUE, <<"TestQueue">>). 20 | -define(DESTINATION, "/amq/queue/TestQueue"). 21 | 22 | all() -> 23 | [{group, version_to_group_name(V)} || V <- ?SUPPORTED_VERSIONS]. 24 | 25 | groups() -> 26 | Tests = [ 27 | publish_no_dest_error, 28 | publish_unauthorized_error, 29 | subscribe_error, 30 | subscribe, 31 | unsubscribe_ack, 32 | subscribe_ack, 33 | send, 34 | delete_queue_subscribe, 35 | temp_destination_queue, 36 | temp_destination_in_send, 37 | blank_destination_in_send 38 | ], 39 | 40 | [{version_to_group_name(V), [sequence], Tests} 41 | || V <- ?SUPPORTED_VERSIONS]. 42 | 43 | version_to_group_name(V) -> 44 | list_to_atom(re:replace("version_" ++ V, 45 | "\\.", 46 | "_", 47 | [global, {return, list}])). 48 | 49 | init_per_suite(Config) -> 50 | Config1 = rabbit_ct_helpers:set_config(Config, 51 | [{rmq_nodename_suffix, ?MODULE}]), 52 | rabbit_ct_helpers:log_environment(), 53 | rabbit_ct_helpers:run_setup_steps(Config1, 54 | rabbit_ct_broker_helpers:setup_steps()). 55 | 56 | end_per_suite(Config) -> 57 | rabbit_ct_helpers:run_teardown_steps(Config, 58 | rabbit_ct_broker_helpers:teardown_steps()). 59 | 60 | init_per_group(Group, Config) -> 61 | Suffix = string:sub_string(atom_to_list(Group), 9), 62 | Version = re:replace(Suffix, "_", ".", [global, {return, list}]), 63 | rabbit_ct_helpers:set_config(Config, [{version, Version}]). 64 | 65 | end_per_group(_Group, Config) -> Config. 66 | 67 | init_per_testcase(TestCase, Config) -> 68 | Version = ?config(version, Config), 69 | StompPort = rabbit_ct_broker_helpers:get_node_config(Config, 0, tcp_port_stomp), 70 | {ok, Connection} = amqp_connection:start(#amqp_params_direct{ 71 | node = rabbit_ct_broker_helpers:get_node_config(Config, 0, nodename) 72 | }), 73 | {ok, Channel} = amqp_connection:open_channel(Connection), 74 | {ok, Client} = rabbit_stomp_client:connect(Version, StompPort), 75 | Config1 = rabbit_ct_helpers:set_config(Config, [ 76 | {amqp_connection, Connection}, 77 | {amqp_channel, Channel}, 78 | {stomp_client, Client} 79 | ]), 80 | init_per_testcase0(TestCase, Config1). 81 | 82 | end_per_testcase(TestCase, Config) -> 83 | Connection = ?config(amqp_connection, Config), 84 | Channel = ?config(amqp_channel, Config), 85 | Client = ?config(stomp_client, Config), 86 | rabbit_stomp_client:disconnect(Client), 87 | amqp_channel:close(Channel), 88 | amqp_connection:close(Connection), 89 | end_per_testcase0(TestCase, Config). 90 | 91 | init_per_testcase0(publish_unauthorized_error, Config) -> 92 | Channel = ?config(amqp_channel, Config), 93 | #'queue.declare_ok'{} = 94 | amqp_channel:call(Channel, #'queue.declare'{queue = <<"RestrictedQueue">>, 95 | auto_delete = true}), 96 | 97 | rabbit_ct_broker_helpers:rpc(Config, 0, rabbit_auth_backend_internal, add_user, 98 | [<<"user">>, <<"pass">>, <<"acting-user">>]), 99 | rabbit_ct_broker_helpers:rpc(Config, 0, rabbit_auth_backend_internal, set_permissions, [ 100 | <<"user">>, <<"/">>, <<"nothing">>, <<"nothing">>, <<"nothing">>, <<"acting-user">>]), 101 | Version = ?config(version, Config), 102 | StompPort = rabbit_ct_broker_helpers:get_node_config(Config, 0, tcp_port_stomp), 103 | {ok, ClientFoo} = rabbit_stomp_client:connect(Version, "user", "pass", StompPort), 104 | rabbit_ct_helpers:set_config(Config, [{client_foo, ClientFoo}]); 105 | init_per_testcase0(_, Config) -> 106 | Config. 107 | 108 | end_per_testcase0(publish_unauthorized_error, Config) -> 109 | ClientFoo = ?config(client_foo, Config), 110 | rabbit_stomp_client:disconnect(ClientFoo), 111 | rabbit_ct_broker_helpers:rpc(Config, 0, rabbit_auth_backend_internal, delete_user, 112 | [<<"user">>, <<"acting-user">>]), 113 | Config; 114 | end_per_testcase0(_, Config) -> 115 | Config. 116 | 117 | publish_no_dest_error(Config) -> 118 | Client = ?config(stomp_client, Config), 119 | rabbit_stomp_client:send( 120 | Client, "SEND", [{"destination", "/exchange/non-existent"}], ["hello"]), 121 | {ok, _Client1, Hdrs, _} = stomp_receive(Client, "ERROR"), 122 | "not_found" = proplists:get_value("message", Hdrs), 123 | ok. 124 | 125 | publish_unauthorized_error(Config) -> 126 | ClientFoo = ?config(client_foo, Config), 127 | rabbit_stomp_client:send( 128 | ClientFoo, "SEND", [{"destination", "/amq/queue/RestrictedQueue"}], ["hello"]), 129 | {ok, _Client1, Hdrs, _} = stomp_receive(ClientFoo, "ERROR"), 130 | "access_refused" = proplists:get_value("message", Hdrs), 131 | ok. 132 | 133 | subscribe_error(Config) -> 134 | Client = ?config(stomp_client, Config), 135 | %% SUBSCRIBE to missing queue 136 | rabbit_stomp_client:send( 137 | Client, "SUBSCRIBE", [{"destination", ?DESTINATION}]), 138 | {ok, _Client1, Hdrs, _} = stomp_receive(Client, "ERROR"), 139 | "not_found" = proplists:get_value("message", Hdrs), 140 | ok. 141 | 142 | subscribe(Config) -> 143 | Channel = ?config(amqp_channel, Config), 144 | Client = ?config(stomp_client, Config), 145 | #'queue.declare_ok'{} = 146 | amqp_channel:call(Channel, #'queue.declare'{queue = ?QUEUE, 147 | auto_delete = true}), 148 | 149 | %% subscribe and wait for receipt 150 | rabbit_stomp_client:send( 151 | Client, "SUBSCRIBE", [{"destination", ?DESTINATION}, {"receipt", "foo"}]), 152 | {ok, Client1, _, _} = stomp_receive(Client, "RECEIPT"), 153 | 154 | %% send from amqp 155 | Method = #'basic.publish'{exchange = <<"">>, routing_key = ?QUEUE}, 156 | 157 | amqp_channel:call(Channel, Method, #amqp_msg{props = #'P_basic'{}, 158 | payload = <<"hello">>}), 159 | 160 | {ok, _Client2, _, [<<"hello">>]} = stomp_receive(Client1, "MESSAGE"), 161 | ok. 162 | 163 | unsubscribe_ack(Config) -> 164 | Channel = ?config(amqp_channel, Config), 165 | Client = ?config(stomp_client, Config), 166 | Version = ?config(version, Config), 167 | #'queue.declare_ok'{} = 168 | amqp_channel:call(Channel, #'queue.declare'{queue = ?QUEUE, 169 | auto_delete = true}), 170 | %% subscribe and wait for receipt 171 | rabbit_stomp_client:send( 172 | Client, "SUBSCRIBE", [{"destination", ?DESTINATION}, 173 | {"receipt", "rcpt1"}, 174 | {"ack", "client"}, 175 | {"id", "subscription-id"}]), 176 | {ok, Client1, _, _} = stomp_receive(Client, "RECEIPT"), 177 | 178 | %% send from amqp 179 | Method = #'basic.publish'{exchange = <<"">>, routing_key = ?QUEUE}, 180 | 181 | amqp_channel:call(Channel, Method, #amqp_msg{props = #'P_basic'{}, 182 | payload = <<"hello">>}), 183 | 184 | {ok, Client2, Hdrs1, [<<"hello">>]} = stomp_receive(Client1, "MESSAGE"), 185 | 186 | rabbit_stomp_client:send( 187 | Client2, "UNSUBSCRIBE", [{"destination", ?DESTINATION}, 188 | {"id", "subscription-id"}]), 189 | 190 | rabbit_stomp_client:send( 191 | Client2, "ACK", [{rabbit_stomp_util:ack_header_name(Version), 192 | proplists:get_value( 193 | rabbit_stomp_util:msg_header_name(Version), Hdrs1)}, 194 | {"receipt", "rcpt2"}]), 195 | 196 | {ok, _Client3, Hdrs2, _Body2} = stomp_receive(Client2, "ERROR"), 197 | ?assertEqual("Subscription not found", 198 | proplists:get_value("message", Hdrs2)), 199 | ok. 200 | 201 | subscribe_ack(Config) -> 202 | Channel = ?config(amqp_channel, Config), 203 | Client = ?config(stomp_client, Config), 204 | Version = ?config(version, Config), 205 | #'queue.declare_ok'{} = 206 | amqp_channel:call(Channel, #'queue.declare'{queue = ?QUEUE, 207 | auto_delete = true}), 208 | 209 | %% subscribe and wait for receipt 210 | rabbit_stomp_client:send( 211 | Client, "SUBSCRIBE", [{"destination", ?DESTINATION}, 212 | {"receipt", "foo"}, 213 | {"ack", "client"}]), 214 | {ok, Client1, _, _} = stomp_receive(Client, "RECEIPT"), 215 | 216 | %% send from amqp 217 | Method = #'basic.publish'{exchange = <<"">>, routing_key = ?QUEUE}, 218 | 219 | amqp_channel:call(Channel, Method, #amqp_msg{props = #'P_basic'{}, 220 | payload = <<"hello">>}), 221 | 222 | {ok, _Client2, Headers, [<<"hello">>]} = stomp_receive(Client1, "MESSAGE"), 223 | false = (Version == "1.2") xor proplists:is_defined(?HEADER_ACK, Headers), 224 | 225 | MsgHeader = rabbit_stomp_util:msg_header_name(Version), 226 | AckValue = proplists:get_value(MsgHeader, Headers), 227 | AckHeader = rabbit_stomp_util:ack_header_name(Version), 228 | 229 | rabbit_stomp_client:send(Client, "ACK", [{AckHeader, AckValue}]), 230 | #'basic.get_empty'{} = 231 | amqp_channel:call(Channel, #'basic.get'{queue = ?QUEUE}), 232 | ok. 233 | 234 | send(Config) -> 235 | Channel = ?config(amqp_channel, Config), 236 | Client = ?config(stomp_client, Config), 237 | #'queue.declare_ok'{} = 238 | amqp_channel:call(Channel, #'queue.declare'{queue = ?QUEUE, 239 | auto_delete = true}), 240 | 241 | %% subscribe and wait for receipt 242 | rabbit_stomp_client:send( 243 | Client, "SUBSCRIBE", [{"destination", ?DESTINATION}, {"receipt", "foo"}]), 244 | {ok, Client1, _, _} = stomp_receive(Client, "RECEIPT"), 245 | 246 | %% send from stomp 247 | rabbit_stomp_client:send( 248 | Client1, "SEND", [{"destination", ?DESTINATION}], ["hello"]), 249 | 250 | {ok, _Client2, _, [<<"hello">>]} = stomp_receive(Client1, "MESSAGE"), 251 | ok. 252 | 253 | delete_queue_subscribe(Config) -> 254 | Channel = ?config(amqp_channel, Config), 255 | Client = ?config(stomp_client, Config), 256 | #'queue.declare_ok'{} = 257 | amqp_channel:call(Channel, #'queue.declare'{queue = ?QUEUE, 258 | auto_delete = true}), 259 | 260 | %% subscribe and wait for receipt 261 | rabbit_stomp_client:send( 262 | Client, "SUBSCRIBE", [{"destination", ?DESTINATION}, {"receipt", "bah"}]), 263 | {ok, Client1, _, _} = stomp_receive(Client, "RECEIPT"), 264 | 265 | %% delete queue while subscribed 266 | #'queue.delete_ok'{} = 267 | amqp_channel:call(Channel, #'queue.delete'{queue = ?QUEUE}), 268 | 269 | {ok, _Client2, Headers, _} = stomp_receive(Client1, "ERROR"), 270 | 271 | ?DESTINATION = proplists:get_value("subscription", Headers), 272 | 273 | % server closes connection 274 | ok. 275 | 276 | temp_destination_queue(Config) -> 277 | Channel = ?config(amqp_channel, Config), 278 | Client = ?config(stomp_client, Config), 279 | #'queue.declare_ok'{} = 280 | amqp_channel:call(Channel, #'queue.declare'{queue = ?QUEUE, 281 | auto_delete = true}), 282 | rabbit_stomp_client:send( Client, "SEND", [{"destination", ?DESTINATION}, 283 | {"reply-to", "/temp-queue/foo"}], 284 | ["ping"]), 285 | amqp_channel:call(Channel,#'basic.consume'{queue = ?QUEUE, no_ack = true}), 286 | receive #'basic.consume_ok'{consumer_tag = _Tag} -> ok end, 287 | ReplyTo = receive {#'basic.deliver'{delivery_tag = _DTag}, 288 | #'amqp_msg'{payload = <<"ping">>, 289 | props = #'P_basic'{reply_to = RT}}} -> RT 290 | end, 291 | ok = amqp_channel:call(Channel, 292 | #'basic.publish'{routing_key = ReplyTo}, 293 | #amqp_msg{payload = <<"pong">>}), 294 | {ok, _Client1, _, [<<"pong">>]} = stomp_receive(Client, "MESSAGE"), 295 | ok. 296 | 297 | temp_destination_in_send(Config) -> 298 | Client = ?config(stomp_client, Config), 299 | rabbit_stomp_client:send( Client, "SEND", [{"destination", "/temp-queue/foo"}], 300 | ["poing"]), 301 | {ok, _Client1, Hdrs, _} = stomp_receive(Client, "ERROR"), 302 | "Invalid destination" = proplists:get_value("message", Hdrs), 303 | ok. 304 | 305 | blank_destination_in_send(Config) -> 306 | Client = ?config(stomp_client, Config), 307 | rabbit_stomp_client:send( Client, "SEND", [{"destination", ""}], 308 | ["poing"]), 309 | {ok, _Client1, Hdrs, _} = stomp_receive(Client, "ERROR"), 310 | "Invalid destination" = proplists:get_value("message", Hdrs), 311 | ok. 312 | 313 | stomp_receive(Client, Command) -> 314 | {#stomp_frame{command = Command, 315 | headers = Hdrs, 316 | body_iolist = Body}, Client1} = 317 | rabbit_stomp_client:recv(Client), 318 | {ok, Client1, Hdrs, Body}. 319 | 320 | -------------------------------------------------------------------------------- /test/command_SUITE.erl: -------------------------------------------------------------------------------- 1 | %% This Source Code Form is subject to the terms of the Mozilla Public 2 | %% License, v. 2.0. If a copy of the MPL was not distributed with this 3 | %% file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | %% 5 | %% Copyright (c) 2007-2020 VMware, Inc. or its affiliates. All rights reserved. 6 | %% 7 | 8 | -module(command_SUITE). 9 | -compile([export_all]). 10 | 11 | -include_lib("common_test/include/ct.hrl"). 12 | -include_lib("eunit/include/eunit.hrl"). 13 | -include_lib("amqp_client/include/amqp_client.hrl"). 14 | -include("rabbit_stomp.hrl"). 15 | 16 | 17 | -define(COMMAND, 'Elixir.RabbitMQ.CLI.Ctl.Commands.ListStompConnectionsCommand'). 18 | 19 | all() -> 20 | [ 21 | {group, non_parallel_tests} 22 | ]. 23 | 24 | groups() -> 25 | [ 26 | {non_parallel_tests, [], [ 27 | merge_defaults, 28 | run 29 | ]} 30 | ]. 31 | 32 | init_per_suite(Config) -> 33 | Config1 = rabbit_ct_helpers:set_config(Config, 34 | [{rmq_nodename_suffix, ?MODULE}]), 35 | rabbit_ct_helpers:log_environment(), 36 | rabbit_ct_helpers:run_setup_steps(Config1, 37 | rabbit_ct_broker_helpers:setup_steps()). 38 | 39 | end_per_suite(Config) -> 40 | rabbit_ct_helpers:run_teardown_steps(Config, 41 | rabbit_ct_broker_helpers:teardown_steps()). 42 | 43 | init_per_group(_, Config) -> 44 | Config. 45 | 46 | end_per_group(_, Config) -> 47 | Config. 48 | 49 | init_per_testcase(Testcase, Config) -> 50 | rabbit_ct_helpers:testcase_started(Config, Testcase). 51 | 52 | end_per_testcase(Testcase, Config) -> 53 | rabbit_ct_helpers:testcase_finished(Config, Testcase). 54 | 55 | merge_defaults(_Config) -> 56 | {[<<"session_id">>, <<"conn_name">>], #{verbose := false}} = 57 | ?COMMAND:merge_defaults([], #{}), 58 | 59 | {[<<"other_key">>], #{verbose := true}} = 60 | ?COMMAND:merge_defaults([<<"other_key">>], #{verbose => true}), 61 | 62 | {[<<"other_key">>], #{verbose := false}} = 63 | ?COMMAND:merge_defaults([<<"other_key">>], #{verbose => false}). 64 | 65 | 66 | run(Config) -> 67 | 68 | Node = rabbit_ct_broker_helpers:get_node_config(Config, 0, nodename), 69 | Opts = #{node => Node, timeout => 10000, verbose => false}, 70 | 71 | %% No connections 72 | [] = 'Elixir.Enum':to_list(?COMMAND:run([], Opts)), 73 | 74 | StompPort = rabbit_ct_broker_helpers:get_node_config(Config, 0, tcp_port_stomp), 75 | 76 | {ok, _Client} = rabbit_stomp_client:connect(StompPort), 77 | ct:sleep(100), 78 | 79 | [[{session_id, _}]] = 80 | 'Elixir.Enum':to_list(?COMMAND:run([<<"session_id">>], Opts)), 81 | 82 | 83 | {ok, _Client2} = rabbit_stomp_client:connect(StompPort), 84 | ct:sleep(100), 85 | 86 | [[{session_id, _}], [{session_id, _}]] = 87 | 'Elixir.Enum':to_list(?COMMAND:run([<<"session_id">>], Opts)), 88 | 89 | Port = rabbit_ct_broker_helpers:get_node_config(Config, 0, tcp_port_amqp), 90 | start_amqp_connection(network, Node, Port), 91 | 92 | %% There are still just two connections 93 | [[{session_id, _}], [{session_id, _}]] = 94 | 'Elixir.Enum':to_list(?COMMAND:run([<<"session_id">>], Opts)), 95 | 96 | start_amqp_connection(direct, Node, Port), 97 | 98 | %% Still two MQTT connections, one direct AMQP 0-9-1 connection 99 | [[{session_id, _}], [{session_id, _}]] = 100 | 'Elixir.Enum':to_list(?COMMAND:run([<<"session_id">>], Opts)), 101 | 102 | %% Verbose returns all keys 103 | Infos = lists:map(fun(El) -> atom_to_binary(El, utf8) end, ?INFO_ITEMS), 104 | AllKeys = 'Elixir.Enum':to_list(?COMMAND:run(Infos, Opts)), 105 | AllKeys = 'Elixir.Enum':to_list(?COMMAND:run([], Opts#{verbose => true})), 106 | 107 | %% There are two connections 108 | [First, _Second] = AllKeys, 109 | 110 | %% Keys are INFO_ITEMS 111 | KeysCount = length(?INFO_ITEMS), 112 | KeysCount = length(First), 113 | 114 | {Keys, _} = lists:unzip(First), 115 | 116 | [] = Keys -- ?INFO_ITEMS, 117 | [] = ?INFO_ITEMS -- Keys. 118 | 119 | 120 | start_amqp_connection(Type, Node, Port) -> 121 | Params = amqp_params(Type, Node, Port), 122 | {ok, _Connection} = amqp_connection:start(Params). 123 | 124 | amqp_params(network, _, Port) -> 125 | #amqp_params_network{port = Port}; 126 | amqp_params(direct, Node, _) -> 127 | #amqp_params_direct{node = Node}. 128 | -------------------------------------------------------------------------------- /test/config_schema_SUITE.erl: -------------------------------------------------------------------------------- 1 | %% This Source Code Form is subject to the terms of the Mozilla Public 2 | %% License, v. 2.0. If a copy of the MPL was not distributed with this 3 | %% file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | %% 5 | %% Copyright (c) 2007-2020 VMware, Inc. or its affiliates. All rights reserved. 6 | %% 7 | 8 | -module(config_schema_SUITE). 9 | 10 | -compile(export_all). 11 | 12 | all() -> 13 | [ 14 | run_snippets 15 | ]. 16 | 17 | %% ------------------------------------------------------------------- 18 | %% Testsuite setup/teardown. 19 | %% ------------------------------------------------------------------- 20 | 21 | init_per_suite(Config) -> 22 | rabbit_ct_helpers:log_environment(), 23 | Config1 = rabbit_ct_helpers:run_setup_steps(Config), 24 | rabbit_ct_config_schema:init_schemas(rabbitmq_stomp, Config1). 25 | 26 | 27 | end_per_suite(Config) -> 28 | rabbit_ct_helpers:run_teardown_steps(Config). 29 | 30 | init_per_testcase(Testcase, Config) -> 31 | rabbit_ct_helpers:testcase_started(Config, Testcase), 32 | Config1 = rabbit_ct_helpers:set_config(Config, [ 33 | {rmq_nodename_suffix, Testcase} 34 | ]), 35 | rabbit_ct_helpers:run_steps(Config1, 36 | rabbit_ct_broker_helpers:setup_steps() ++ 37 | rabbit_ct_client_helpers:setup_steps()). 38 | 39 | end_per_testcase(Testcase, Config) -> 40 | Config1 = rabbit_ct_helpers:run_steps(Config, 41 | rabbit_ct_client_helpers:teardown_steps() ++ 42 | rabbit_ct_broker_helpers:teardown_steps()), 43 | rabbit_ct_helpers:testcase_finished(Config1, Testcase). 44 | 45 | %% ------------------------------------------------------------------- 46 | %% Testcases. 47 | %% ------------------------------------------------------------------- 48 | 49 | run_snippets(Config) -> 50 | ok = rabbit_ct_broker_helpers:rpc(Config, 0, 51 | ?MODULE, run_snippets1, [Config]). 52 | 53 | run_snippets1(Config) -> 54 | rabbit_ct_config_schema:run_snippets(Config). 55 | 56 | -------------------------------------------------------------------------------- /test/config_schema_SUITE_data/certs/cacert.pem: -------------------------------------------------------------------------------- 1 | I'm not a certificate 2 | -------------------------------------------------------------------------------- /test/config_schema_SUITE_data/certs/cert.pem: -------------------------------------------------------------------------------- 1 | I'm not a certificate 2 | -------------------------------------------------------------------------------- /test/config_schema_SUITE_data/certs/key.pem: -------------------------------------------------------------------------------- 1 | I'm not a certificate 2 | -------------------------------------------------------------------------------- /test/config_schema_SUITE_data/rabbitmq_stomp.snippets: -------------------------------------------------------------------------------- 1 | [{listener_port, 2 | "stomp.listeners.tcp.1 = 12345", 3 | [{rabbitmq_stomp,[{tcp_listeners,[12345]}]}], 4 | [rabbitmq_stomp]}, 5 | {listeners_ip, 6 | "stomp.listeners.tcp.1 = 127.0.0.1:61613 7 | stomp.listeners.tcp.2 = ::1:61613", 8 | [{rabbitmq_stomp,[{tcp_listeners,[{"127.0.0.1",61613},{"::1",61613}]}]}], 9 | [rabbitmq_stomp]}, 10 | 11 | {listener_tcp_options, 12 | "stomp.listeners.tcp.1 = 127.0.0.1:61613 13 | stomp.listeners.tcp.2 = ::1:61613 14 | 15 | stomp.tcp_listen_options.backlog = 2048 16 | stomp.tcp_listen_options.recbuf = 8192 17 | stomp.tcp_listen_options.sndbuf = 8192 18 | 19 | stomp.tcp_listen_options.keepalive = true 20 | stomp.tcp_listen_options.nodelay = true 21 | 22 | stomp.tcp_listen_options.exit_on_close = true 23 | 24 | stomp.tcp_listen_options.send_timeout = 120 25 | ", 26 | [{rabbitmq_stomp,[ 27 | {tcp_listeners,[ 28 | {"127.0.0.1",61613}, 29 | {"::1",61613} 30 | ]} 31 | , {tcp_listen_options, [ 32 | {backlog, 2048}, 33 | {exit_on_close, true}, 34 | 35 | {recbuf, 8192}, 36 | {sndbuf, 8192}, 37 | 38 | {send_timeout, 120}, 39 | 40 | {keepalive, true}, 41 | {nodelay, true} 42 | ]} 43 | ]}], 44 | [rabbitmq_stomp]}, 45 | 46 | {ssl, 47 | "ssl_options.cacertfile = test/config_schema_SUITE_data/certs/cacert.pem 48 | ssl_options.certfile = test/config_schema_SUITE_data/certs/cert.pem 49 | ssl_options.keyfile = test/config_schema_SUITE_data/certs/key.pem 50 | ssl_options.verify = verify_peer 51 | ssl_options.fail_if_no_peer_cert = true 52 | 53 | stomp.listeners.tcp.1 = 61613 54 | stomp.listeners.ssl.1 = 61614", 55 | [{rabbit, 56 | [{ssl_options, 57 | [{cacertfile,"test/config_schema_SUITE_data/certs/cacert.pem"}, 58 | {certfile,"test/config_schema_SUITE_data/certs/cert.pem"}, 59 | {keyfile,"test/config_schema_SUITE_data/certs/key.pem"}, 60 | {verify,verify_peer}, 61 | {fail_if_no_peer_cert,true}]}]}, 62 | {rabbitmq_stomp,[{tcp_listeners,[61613]},{ssl_listeners,[61614]}]}], 63 | [rabbitmq_stomp]}, 64 | {defaults, 65 | "stomp.default_user = guest 66 | stomp.default_pass = guest 67 | stomp.proxy_protocol = false 68 | stomp.hide_server_info = false", 69 | [{rabbitmq_stomp,[{default_user,[{login,"guest"},{passcode,"guest"}]}, 70 | {proxy_protocol,false},{hide_server_info,false}]}], 71 | [rabbitmq_stomp]}, 72 | {ssl_cert_login, 73 | "stomp.ssl_cert_login = true", 74 | [{rabbitmq_stomp,[{ssl_cert_login,true}]}], 75 | [rabbitmq_stomp]}, 76 | {proxy_protocol, 77 | "stomp.default_user = guest 78 | stomp.default_pass = guest 79 | stomp.implicit_connect = true 80 | stomp.proxy_protocol = true", 81 | [{rabbitmq_stomp,[{default_user,[{login,"guest"},{passcode,"guest"}]}, 82 | {implicit_connect,true}, 83 | {proxy_protocol,true}]}], 84 | [rabbitmq_stomp]}, 85 | {default_vhost, 86 | "stomp.default_vhost = /", 87 | [{rabbitmq_stomp,[{default_vhost,<<"/">>}]}], 88 | [rabbitmq_stomp]}, 89 | {default_topic_exchange, 90 | "stomp.default_topic_exchange = my.fancy.topic", 91 | [{rabbitmq_stomp,[{default_topic_exchange,<<"my.fancy.topic">>}]}], 92 | [rabbitmq_stomp]}, 93 | {hide_server_info, 94 | "stomp.hide_server_info = true", 95 | [{rabbitmq_stomp,[{hide_server_info,true}]}], 96 | [rabbitmq_stomp]} 97 | ]. 98 | -------------------------------------------------------------------------------- /test/connections_SUITE.erl: -------------------------------------------------------------------------------- 1 | %% This Source Code Form is subject to the terms of the Mozilla Public 2 | %% License, v. 2.0. If a copy of the MPL was not distributed with this 3 | %% file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | %% 5 | %% Copyright (c) 2007-2020 VMware, Inc. or its affiliates. All rights reserved. 6 | %% 7 | 8 | -module(connections_SUITE). 9 | -compile(export_all). 10 | 11 | -import(rabbit_misc, [pget/2]). 12 | 13 | -include_lib("common_test/include/ct.hrl"). 14 | -include_lib("amqp_client/include/amqp_client.hrl"). 15 | -include("rabbit_stomp_frame.hrl"). 16 | -define(DESTINATION, "/queue/bulk-test"). 17 | 18 | all() -> 19 | [ 20 | messages_not_dropped_on_disconnect, 21 | direct_client_connections_are_not_leaked, 22 | stats_are_not_leaked, 23 | stats, 24 | heartbeat 25 | ]. 26 | 27 | merge_app_env(Config) -> 28 | rabbit_ct_helpers:merge_app_env(Config, 29 | {rabbit, [ 30 | {collect_statistics, basic}, 31 | {collect_statistics_interval, 100} 32 | ]}). 33 | 34 | init_per_suite(Config) -> 35 | Config1 = rabbit_ct_helpers:set_config(Config, 36 | [{rmq_nodename_suffix, ?MODULE}]), 37 | rabbit_ct_helpers:log_environment(), 38 | rabbit_ct_helpers:run_setup_steps(Config1, 39 | [ fun merge_app_env/1 ] ++ 40 | rabbit_ct_broker_helpers:setup_steps()). 41 | 42 | 43 | end_per_suite(Config) -> 44 | rabbit_ct_helpers:run_teardown_steps(Config, 45 | rabbit_ct_broker_helpers:teardown_steps()). 46 | 47 | -define(GARBAGE, <<"bdaf63dda9d78b075c748b740e7c3510ad203b07\nbdaf63dd">>). 48 | 49 | count_connections(Config) -> 50 | StompPort = get_stomp_port(Config), 51 | %% The default port is 61613 but it's in the middle of the ephemeral 52 | %% ports range on many operating systems. Therefore, there is a 53 | %% chance this port is already in use. Let's use a port close to the 54 | %% AMQP default port. 55 | IPv4Count = try 56 | %% Count IPv4 connections. On some platforms, the IPv6 listener 57 | %% implicitely listens to IPv4 connections too so the IPv4 58 | %% listener doesn't exist. Thus this try/catch. This is the case 59 | %% with Linux where net.ipv6.bindv6only is disabled (default in 60 | %% most cases). 61 | rpc_count_connections(Config, {acceptor, {0,0,0,0}, StompPort}) 62 | catch 63 | _:{badarg, _} -> 0; 64 | _:Other -> exit({foo, Other}) 65 | end, 66 | IPv6Count = try 67 | %% Count IPv6 connections. We also use a try/catch block in case 68 | %% the host is not configured for IPv6. 69 | rpc_count_connections(Config, {acceptor, {0,0,0,0,0,0,0,0}, StompPort}) 70 | catch 71 | _:{badarg, _} -> 0; 72 | _:Other1 -> exit({foo, Other1}) 73 | end, 74 | IPv4Count + IPv6Count. 75 | 76 | rpc_count_connections(Config, ConnSpec) -> 77 | rabbit_ct_broker_helpers:rpc(Config, 0, 78 | ranch_server, count_connections, [ConnSpec]). 79 | 80 | direct_client_connections_are_not_leaked(Config) -> 81 | StompPort = get_stomp_port(Config), 82 | N = count_connections(Config), 83 | lists:foreach(fun (_) -> 84 | {ok, Client = {Socket, _}} = rabbit_stomp_client:connect(StompPort), 85 | %% send garbage which trips up the parser 86 | gen_tcp:send(Socket, ?GARBAGE), 87 | rabbit_stomp_client:send( 88 | Client, "LOL", [{"", ""}]) 89 | end, 90 | lists:seq(1, 100)), 91 | timer:sleep(5000), 92 | N = count_connections(Config), 93 | ok. 94 | 95 | messages_not_dropped_on_disconnect(Config) -> 96 | StompPort = get_stomp_port(Config), 97 | N = count_connections(Config), 98 | {ok, Client} = rabbit_stomp_client:connect(StompPort), 99 | N1 = N + 1, 100 | N1 = count_connections(Config), 101 | [rabbit_stomp_client:send( 102 | Client, "SEND", [{"destination", ?DESTINATION}], 103 | [integer_to_list(Count)]) || Count <- lists:seq(1, 1000)], 104 | rabbit_stomp_client:disconnect(Client), 105 | QName = rabbit_misc:r(<<"/">>, queue, <<"bulk-test">>), 106 | timer:sleep(3000), 107 | N = count_connections(Config), 108 | {ok, Q} = rabbit_ct_broker_helpers:rpc(Config, 0, rabbit_amqqueue, lookup, [QName]), 109 | Messages = rabbit_ct_broker_helpers:rpc(Config, 0, rabbit_amqqueue, info, [Q, [messages]]), 110 | 1000 = pget(messages, Messages), 111 | ok. 112 | 113 | get_stomp_port(Config) -> 114 | rabbit_ct_broker_helpers:get_node_config(Config, 0, tcp_port_stomp). 115 | 116 | stats_are_not_leaked(Config) -> 117 | StompPort = get_stomp_port(Config), 118 | N = rabbit_ct_broker_helpers:rpc(Config, 0, ets, info, [connection_metrics, size]), 119 | {ok, C} = gen_tcp:connect("localhost", StompPort, []), 120 | Bin = <<"GET / HTTP/1.1\r\nHost: www.rabbitmq.com\r\nUser-Agent: curl/7.43.0\r\nAccept: */*\n\n">>, 121 | gen_tcp:send(C, Bin), 122 | gen_tcp:close(C), 123 | timer:sleep(1000), %% Wait for stats to be emitted, which it does every 100ms 124 | N = rabbit_ct_broker_helpers:rpc(Config, 0, ets, info, [connection_metrics, size]), 125 | ok. 126 | 127 | stats(Config) -> 128 | StompPort = get_stomp_port(Config), 129 | {ok, Client} = rabbit_stomp_client:connect(StompPort), 130 | timer:sleep(1000), %% Wait for stats to be emitted, which it does every 100ms 131 | %% Retrieve the connection Pid 132 | [Reader] = rabbit_ct_broker_helpers:rpc(Config, 0, rabbit_stomp, list, []), 133 | [{_, Pid}] = rabbit_ct_broker_helpers:rpc(Config, 0, rabbit_stomp_reader, 134 | info, [Reader, [connection]]), 135 | %% Verify the content of the metrics, garbage_collection must be present 136 | [{Pid, Props}] = rabbit_ct_broker_helpers:rpc(Config, 0, ets, lookup, 137 | [connection_metrics, Pid]), 138 | true = proplists:is_defined(garbage_collection, Props), 139 | 0 = proplists:get_value(timeout, Props), 140 | %% If the coarse entry is present, stats were successfully emitted 141 | [{Pid, _, _, _, _}] = rabbit_ct_broker_helpers:rpc(Config, 0, ets, lookup, 142 | [connection_coarse_metrics, Pid]), 143 | rabbit_stomp_client:disconnect(Client), 144 | ok. 145 | 146 | heartbeat(Config) -> 147 | StompPort = get_stomp_port(Config), 148 | {ok, Client} = rabbit_stomp_client:connect("1.2", "guest", "guest", StompPort, 149 | [{"heart-beat", "5000,7000"}]), 150 | timer:sleep(1000), %% Wait for stats to be emitted, which it does every 100ms 151 | %% Retrieve the connection Pid 152 | [Reader] = rabbit_ct_broker_helpers:rpc(Config, 0, rabbit_stomp, list, []), 153 | [{_, Pid}] = rabbit_ct_broker_helpers:rpc(Config, 0, rabbit_stomp_reader, 154 | info, [Reader, [connection]]), 155 | %% Verify the content of the heartbeat timeout 156 | [{Pid, Props}] = rabbit_ct_broker_helpers:rpc(Config, 0, ets, lookup, 157 | [connection_metrics, Pid]), 158 | 5 = proplists:get_value(timeout, Props), 159 | rabbit_stomp_client:disconnect(Client), 160 | ok. 161 | -------------------------------------------------------------------------------- /test/frame_SUITE.erl: -------------------------------------------------------------------------------- 1 | %% This Source Code Form is subject to the terms of the Mozilla Public 2 | %% License, v. 2.0. If a copy of the MPL was not distributed with this 3 | %% file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | %% 5 | %% Copyright (c) 2007-2020 VMware, Inc. or its affiliates. All rights reserved. 6 | %% 7 | 8 | -module(frame_SUITE). 9 | 10 | -include_lib("common_test/include/ct.hrl"). 11 | -include_lib("eunit/include/eunit.hrl"). 12 | -include_lib("amqp_client/include/amqp_client.hrl"). 13 | -include("rabbit_stomp_frame.hrl"). 14 | -include("rabbit_stomp_headers.hrl"). 15 | -compile(export_all). 16 | 17 | all() -> 18 | [ 19 | parse_simple_frame, 20 | parse_simple_frame_crlf, 21 | parse_command_only, 22 | parse_command_prefixed_with_newline, 23 | parse_ignore_empty_frames, 24 | parse_heartbeat_interframe, 25 | parse_crlf_interframe, 26 | parse_carriage_return_not_ignored_interframe, 27 | parse_carriage_return_mid_command, 28 | parse_carriage_return_end_command, 29 | parse_resume_mid_command, 30 | parse_resume_mid_header_key, 31 | parse_resume_mid_header_val, 32 | parse_resume_mid_body, 33 | parse_no_header_stripping, 34 | parse_multiple_headers, 35 | header_no_colon, 36 | no_nested_escapes, 37 | header_name_with_cr, 38 | header_value_with_cr, 39 | header_value_with_colon, 40 | headers_escaping_roundtrip, 41 | headers_escaping_roundtrip_without_trailing_lf 42 | ]. 43 | 44 | parse_simple_frame(_) -> 45 | parse_simple_frame_gen("\n"). 46 | 47 | parse_simple_frame_crlf(_) -> 48 | parse_simple_frame_gen("\r\n"). 49 | 50 | parse_simple_frame_gen(Term) -> 51 | Headers = [{"header1", "value1"}, {"header2", "value2"}], 52 | Content = frame_string("COMMAND", 53 | Headers, 54 | "Body Content", 55 | Term), 56 | {"COMMAND", Frame, _State} = parse_complete(Content), 57 | [?assertEqual({ok, Value}, 58 | rabbit_stomp_frame:header(Frame, Key)) || 59 | {Key, Value} <- Headers], 60 | #stomp_frame{body_iolist = Body} = Frame, 61 | ?assertEqual(<<"Body Content">>, iolist_to_binary(Body)). 62 | 63 | parse_command_only(_) -> 64 | {ok, #stomp_frame{command = "COMMAND"}, _Rest} = parse("COMMAND\n\n\0"). 65 | 66 | parse_command_prefixed_with_newline(_) -> 67 | {ok, #stomp_frame{command = "COMMAND"}, _Rest} = parse("\nCOMMAND\n\n\0"). 68 | 69 | parse_ignore_empty_frames(_) -> 70 | {ok, #stomp_frame{command = "COMMAND"}, _Rest} = parse("\0\0COMMAND\n\n\0"). 71 | 72 | parse_heartbeat_interframe(_) -> 73 | {ok, #stomp_frame{command = "COMMAND"}, _Rest} = parse("\nCOMMAND\n\n\0"). 74 | 75 | parse_crlf_interframe(_) -> 76 | {ok, #stomp_frame{command = "COMMAND"}, _Rest} = parse("\r\nCOMMAND\n\n\0"). 77 | 78 | parse_carriage_return_not_ignored_interframe(_) -> 79 | {error, {unexpected_chars_between_frames, "\rC"}} = parse("\rCOMMAND\n\n\0"). 80 | 81 | parse_carriage_return_mid_command(_) -> 82 | {error, {unexpected_chars_in_command, "\rA"}} = parse("COMM\rAND\n\n\0"). 83 | 84 | parse_carriage_return_end_command(_) -> 85 | {error, {unexpected_chars_in_command, "\r\r"}} = parse("COMMAND\r\r\n\n\0"). 86 | 87 | parse_resume_mid_command(_) -> 88 | First = "COMM", 89 | Second = "AND\n\n\0", 90 | {more, Resume} = parse(First), 91 | {ok, #stomp_frame{command = "COMMAND"}, _Rest} = parse(Second, Resume). 92 | 93 | parse_resume_mid_header_key(_) -> 94 | First = "COMMAND\nheade", 95 | Second = "r1:value1\n\n\0", 96 | {more, Resume} = parse(First), 97 | {ok, Frame = #stomp_frame{command = "COMMAND"}, _Rest} = 98 | parse(Second, Resume), 99 | ?assertEqual({ok, "value1"}, 100 | rabbit_stomp_frame:header(Frame, "header1")). 101 | 102 | parse_resume_mid_header_val(_) -> 103 | First = "COMMAND\nheader1:val", 104 | Second = "ue1\n\n\0", 105 | {more, Resume} = parse(First), 106 | {ok, Frame = #stomp_frame{command = "COMMAND"}, _Rest} = 107 | parse(Second, Resume), 108 | ?assertEqual({ok, "value1"}, 109 | rabbit_stomp_frame:header(Frame, "header1")). 110 | 111 | parse_resume_mid_body(_) -> 112 | First = "COMMAND\n\nABC", 113 | Second = "DEF\0", 114 | {more, Resume} = parse(First), 115 | {ok, #stomp_frame{command = "COMMAND", body_iolist = Body}, _Rest} = 116 | parse(Second, Resume), 117 | ?assertEqual([<<"ABC">>, <<"DEF">>], Body). 118 | 119 | parse_no_header_stripping(_) -> 120 | Content = "COMMAND\nheader: foo \n\n\0", 121 | {ok, Frame, _} = parse(Content), 122 | {ok, Val} = rabbit_stomp_frame:header(Frame, "header"), 123 | ?assertEqual(" foo ", Val). 124 | 125 | parse_multiple_headers(_) -> 126 | Content = "COMMAND\nheader:correct\nheader:incorrect\n\n\0", 127 | {ok, Frame, _} = parse(Content), 128 | {ok, Val} = rabbit_stomp_frame:header(Frame, "header"), 129 | ?assertEqual("correct", Val). 130 | 131 | header_no_colon(_) -> 132 | Content = "COMMAND\n" 133 | "hdr1:val1\n" 134 | "hdrerror\n" 135 | "hdr2:val2\n" 136 | "\n\0", 137 | ?assertEqual(parse(Content), {error, {header_no_value, "hdrerror"}}). 138 | 139 | no_nested_escapes(_) -> 140 | Content = "COM\\\\rAND\n" % no escapes 141 | "hdr\\\\rname:" % one escape 142 | "hdr\\\\rval\n\n\0", % one escape 143 | {ok, Frame, _} = parse(Content), 144 | ?assertEqual(Frame, 145 | #stomp_frame{command = "COM\\\\rAND", 146 | headers = [{"hdr\\rname", "hdr\\rval"}], 147 | body_iolist = []}). 148 | 149 | header_name_with_cr(_) -> 150 | Content = "COMMAND\nhead\rer:val\n\n\0", 151 | {error, {unexpected_chars_in_header, "\re"}} = parse(Content). 152 | 153 | header_value_with_cr(_) -> 154 | Content = "COMMAND\nheader:val\rue\n\n\0", 155 | {error, {unexpected_chars_in_header, "\ru"}} = parse(Content). 156 | 157 | header_value_with_colon(_) -> 158 | Content = "COMMAND\nheader:val:ue\n\n\0", 159 | {ok, Frame, _} = parse(Content), 160 | ?assertEqual(Frame, 161 | #stomp_frame{ command = "COMMAND", 162 | headers = [{"header", "val:ue"}], 163 | body_iolist = []}). 164 | 165 | test_frame_serialization(Expected, TrailingLF) -> 166 | {ok, Frame, _} = parse(Expected), 167 | {ok, Val} = rabbit_stomp_frame:header(Frame, "head\r:\ner"), 168 | ?assertEqual(":\n\r\\", Val), 169 | Serialized = lists:flatten(rabbit_stomp_frame:serialize(Frame, TrailingLF)), 170 | ?assertEqual(Expected, rabbit_misc:format("~s", [Serialized])). 171 | 172 | headers_escaping_roundtrip(_) -> 173 | test_frame_serialization("COMMAND\nhead\\r\\c\\ner:\\c\\n\\r\\\\\n\n\0\n", true). 174 | 175 | headers_escaping_roundtrip_without_trailing_lf(_) -> 176 | test_frame_serialization("COMMAND\nhead\\r\\c\\ner:\\c\\n\\r\\\\\n\n\0", false). 177 | 178 | parse(Content) -> 179 | parse(Content, rabbit_stomp_frame:initial_state()). 180 | parse(Content, State) -> 181 | rabbit_stomp_frame:parse(list_to_binary(Content), State). 182 | 183 | parse_complete(Content) -> 184 | {ok, Frame = #stomp_frame{command = Command}, State} = parse(Content), 185 | {Command, Frame, State}. 186 | 187 | frame_string(Command, Headers, BodyContent, Term) -> 188 | HeaderString = 189 | lists:flatten([Key ++ ":" ++ Value ++ Term || {Key, Value} <- Headers]), 190 | Command ++ Term ++ HeaderString ++ Term ++ BodyContent ++ "\0" ++ "\n". 191 | 192 | -------------------------------------------------------------------------------- /test/proxy_protocol_SUITE.erl: -------------------------------------------------------------------------------- 1 | %% This Source Code Form is subject to the terms of the Mozilla Public 2 | %% License, v. 2.0. If a copy of the MPL was not distributed with this 3 | %% file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | %% 5 | %% Copyright (c) 2007-2020 VMware, Inc. or its affiliates. All rights reserved. 6 | %% 7 | 8 | -module(proxy_protocol_SUITE). 9 | -compile([export_all]). 10 | 11 | -include_lib("common_test/include/ct.hrl"). 12 | -include_lib("eunit/include/eunit.hrl"). 13 | 14 | -define(TIMEOUT, 5000). 15 | 16 | all() -> 17 | [ 18 | {group, non_parallel_tests} 19 | ]. 20 | 21 | groups() -> 22 | [ 23 | {non_parallel_tests, [], [ 24 | proxy_protocol, 25 | proxy_protocol_tls 26 | ]} 27 | ]. 28 | 29 | init_per_suite(Config) -> 30 | rabbit_ct_helpers:log_environment(), 31 | Suffix = rabbit_ct_helpers:testcase_absname(Config, "", "-"), 32 | Config1 = rabbit_ct_helpers:set_config(Config, [ 33 | {rmq_nodename_suffix, Suffix}, 34 | {rmq_certspwd, "bunnychow"}, 35 | {rabbitmq_ct_tls_verify, verify_none} 36 | ]), 37 | MqttConfig = stomp_config(), 38 | rabbit_ct_helpers:run_setup_steps(Config1, 39 | [ fun(Conf) -> merge_app_env(MqttConfig, Conf) end ] ++ 40 | rabbit_ct_broker_helpers:setup_steps() ++ 41 | rabbit_ct_client_helpers:setup_steps()). 42 | 43 | stomp_config() -> 44 | {rabbitmq_stomp, [ 45 | {proxy_protocol, true} 46 | ]}. 47 | 48 | end_per_suite(Config) -> 49 | rabbit_ct_helpers:run_teardown_steps(Config, 50 | rabbit_ct_client_helpers:teardown_steps() ++ 51 | rabbit_ct_broker_helpers:teardown_steps()). 52 | 53 | init_per_group(_, Config) -> Config. 54 | end_per_group(_, Config) -> Config. 55 | 56 | init_per_testcase(Testcase, Config) -> 57 | rabbit_ct_helpers:testcase_started(Config, Testcase). 58 | 59 | end_per_testcase(Testcase, Config) -> 60 | rabbit_ct_helpers:testcase_finished(Config, Testcase). 61 | 62 | proxy_protocol(Config) -> 63 | Port = rabbit_ct_broker_helpers:get_node_config(Config, 0, tcp_port_stomp), 64 | {ok, Socket} = gen_tcp:connect({127,0,0,1}, Port, 65 | [binary, {active, false}, {packet, raw}]), 66 | ok = inet:send(Socket, "PROXY TCP4 192.168.1.1 192.168.1.2 80 81\r\n"), 67 | ok = inet:send(Socket, stomp_connect_frame()), 68 | {ok, _Packet} = gen_tcp:recv(Socket, 0, ?TIMEOUT), 69 | ConnectionName = rabbit_ct_broker_helpers:rpc(Config, 0, 70 | ?MODULE, connection_name, []), 71 | match = re:run(ConnectionName, <<"^192.168.1.1:80 ">>, [{capture, none}]), 72 | gen_tcp:close(Socket), 73 | ok. 74 | 75 | proxy_protocol_tls(Config) -> 76 | app_utils:start_applications([asn1, crypto, public_key, ssl]), 77 | Port = rabbit_ct_broker_helpers:get_node_config(Config, 0, tcp_port_stomp_tls), 78 | {ok, Socket} = gen_tcp:connect({127,0,0,1}, Port, 79 | [binary, {active, false}, {packet, raw}]), 80 | ok = inet:send(Socket, "PROXY TCP4 192.168.1.1 192.168.1.2 80 81\r\n"), 81 | {ok, SslSocket} = ssl:connect(Socket, [], ?TIMEOUT), 82 | ok = ssl:send(SslSocket, stomp_connect_frame()), 83 | {ok, _Packet} = ssl:recv(SslSocket, 0, ?TIMEOUT), 84 | ConnectionName = rabbit_ct_broker_helpers:rpc(Config, 0, 85 | ?MODULE, connection_name, []), 86 | match = re:run(ConnectionName, <<"^192.168.1.1:80 ">>, [{capture, none}]), 87 | gen_tcp:close(Socket), 88 | ok. 89 | 90 | connection_name() -> 91 | Connections = ets:tab2list(connection_created), 92 | {_Key, Values} = lists:nth(1, Connections), 93 | {_, Name} = lists:keyfind(name, 1, Values), 94 | Name. 95 | 96 | merge_app_env(MqttConfig, Config) -> 97 | rabbit_ct_helpers:merge_app_env(Config, MqttConfig). 98 | 99 | stomp_connect_frame() -> 100 | <<"CONNECT\n", 101 | "login:guest\n", 102 | "passcode:guest\n", 103 | "\n", 104 | 0>>. -------------------------------------------------------------------------------- /test/python_SUITE.erl: -------------------------------------------------------------------------------- 1 | %% This Source Code Form is subject to the terms of the Mozilla Public 2 | %% License, v. 2.0. If a copy of the MPL was not distributed with this 3 | %% file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | %% 5 | %% Copyright (c) 2007-2020 VMware, Inc. or its affiliates. All rights reserved. 6 | %% 7 | 8 | -module(python_SUITE). 9 | -compile(export_all). 10 | -include_lib("common_test/include/ct.hrl"). 11 | 12 | all() -> 13 | [ 14 | common, 15 | ssl, 16 | connect_options 17 | ]. 18 | 19 | init_per_testcase(TestCase, Config) -> 20 | Suffix = rabbit_ct_helpers:testcase_absname(Config, TestCase, "-"), 21 | Config1 = rabbit_ct_helpers:set_config(Config, 22 | [{rmq_certspwd, "bunnychow"}, 23 | {rmq_nodename_suffix, Suffix}]), 24 | rabbit_ct_helpers:log_environment(), 25 | Config2 = rabbit_ct_helpers:run_setup_steps( 26 | Config1, 27 | rabbit_ct_broker_helpers:setup_steps()), 28 | DataDir = ?config(data_dir, Config2), 29 | PikaDir = filename:join([DataDir, "deps", "pika"]), 30 | StomppyDir = filename:join([DataDir, "deps", "stomppy"]), 31 | rabbit_ct_helpers:make(Config2, PikaDir, []), 32 | rabbit_ct_helpers:make(Config2, StomppyDir, []), 33 | Config2. 34 | 35 | end_per_testcase(_, Config) -> 36 | rabbit_ct_helpers:run_teardown_steps(Config, 37 | rabbit_ct_broker_helpers:teardown_steps()). 38 | 39 | 40 | common(Config) -> 41 | run(Config, filename:join("src", "test.py")). 42 | 43 | connect_options(Config) -> 44 | run(Config, filename:join("src", "test_connect_options.py")). 45 | 46 | ssl(Config) -> 47 | run(Config, filename:join("src", "test_ssl.py")). 48 | 49 | run(Config, Test) -> 50 | DataDir = ?config(data_dir, Config), 51 | CertsDir = rabbit_ct_helpers:get_config(Config, rmq_certsdir), 52 | StompPort = rabbit_ct_broker_helpers:get_node_config(Config, 0, tcp_port_stomp), 53 | StompPortTls = rabbit_ct_broker_helpers:get_node_config(Config, 0, tcp_port_stomp_tls), 54 | AmqpPort = rabbit_ct_broker_helpers:get_node_config(Config, 0, tcp_port_amqp), 55 | NodeName = rabbit_ct_broker_helpers:get_node_config(Config, 0, nodename), 56 | PythonPath = os:getenv("PYTHONPATH"), 57 | os:putenv("PYTHONPATH", filename:join([DataDir, "deps", "pika","pika"]) 58 | ++":"++ 59 | filename:join([DataDir, "deps", "stomppy", "stomppy"]) 60 | ++ ":" ++ 61 | PythonPath), 62 | os:putenv("AMQP_PORT", integer_to_list(AmqpPort)), 63 | os:putenv("STOMP_PORT", integer_to_list(StompPort)), 64 | os:putenv("STOMP_PORT_TLS", integer_to_list(StompPortTls)), 65 | os:putenv("RABBITMQ_NODENAME", atom_to_list(NodeName)), 66 | os:putenv("SSL_CERTS_PATH", CertsDir), 67 | {ok, _} = rabbit_ct_helpers:exec([filename:join(DataDir, Test)]). 68 | 69 | 70 | cur_dir() -> 71 | {ok, Src} = filelib:find_source(?MODULE), 72 | filename:dirname(Src). 73 | -------------------------------------------------------------------------------- /test/python_SUITE_data/deps/pika/Makefile: -------------------------------------------------------------------------------- 1 | UPSTREAM_GIT=https://github.com/pika/pika.git 2 | REVISION=1.1.0 3 | 4 | LIB_DIR=pika 5 | CHECKOUT_DIR=pika-$(REVISION) 6 | 7 | TARGETS=$(LIB_DIR) 8 | 9 | all: $(TARGETS) 10 | 11 | clean: 12 | rm -rf $(LIB_DIR) 13 | 14 | distclean: clean 15 | rm -rf $(CHECKOUT_DIR) 16 | 17 | $(LIB_DIR) : $(CHECKOUT_DIR) 18 | rm -rf $@ 19 | cp -R $< $@ 20 | 21 | $(CHECKOUT_DIR): 22 | git clone --depth 1 --branch $(REVISION) $(UPSTREAM_GIT) $@ || \ 23 | (rm -rf $@; exit 1) 24 | 25 | echo-revision: 26 | @echo $(REVISION) 27 | 28 | -------------------------------------------------------------------------------- /test/python_SUITE_data/deps/stomppy/Makefile: -------------------------------------------------------------------------------- 1 | UPSTREAM_GIT=https://github.com/jasonrbriggs/stomp.py.git 2 | REVISION=v4.0.16 3 | 4 | LIB_DIR=stomppy 5 | CHECKOUT_DIR=stomppy-git 6 | 7 | TARGETS=$(LIB_DIR) 8 | 9 | all: $(TARGETS) 10 | 11 | clean: 12 | rm -rf $(LIB_DIR) 13 | 14 | distclean: clean 15 | rm -rf $(CHECKOUT_DIR) 16 | 17 | $(LIB_DIR) : $(CHECKOUT_DIR) 18 | rm -rf $@ 19 | cp -R $< $@ 20 | 21 | $(CHECKOUT_DIR): 22 | git clone $(UPSTREAM_GIT) $@ 23 | (cd $@ && git checkout $(REVISION)) || rm -rf $@ 24 | 25 | echo-revision: 26 | @echo $(REVISION) 27 | 28 | -------------------------------------------------------------------------------- /test/python_SUITE_data/src/ack.py: -------------------------------------------------------------------------------- 1 | ## This Source Code Form is subject to the terms of the Mozilla Public 2 | ## License, v. 2.0. If a copy of the MPL was not distributed with this 3 | ## file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | ## 5 | ## Copyright (c) 2007-2020 VMware, Inc. or its affiliates. All rights reserved. 6 | ## 7 | 8 | import unittest 9 | import stomp 10 | import base 11 | import time 12 | import os 13 | 14 | class TestAck(base.BaseTest): 15 | 16 | def test_ack_client(self): 17 | destination = "/queue/ack-test" 18 | 19 | # subscribe and send message 20 | self.listener.reset(2) ## expecting 2 messages 21 | self.subscribe_dest(self.conn, destination, None, 22 | ack='client', 23 | headers={'prefetch-count': '10'}) 24 | self.conn.send(destination, "test1") 25 | self.conn.send(destination, "test2") 26 | self.assertTrue(self.listener.wait(4), "initial message not received") 27 | self.assertEquals(2, len(self.listener.messages)) 28 | 29 | # disconnect with no ack 30 | self.conn.disconnect() 31 | 32 | # now reconnect 33 | conn2 = self.create_connection() 34 | try: 35 | listener2 = base.WaitableListener() 36 | listener2.reset(2) 37 | conn2.set_listener('', listener2) 38 | self.subscribe_dest(conn2, destination, None, 39 | ack='client', 40 | headers={'prefetch-count': '10'}) 41 | self.assertTrue(listener2.wait(), "message not received again") 42 | self.assertEquals(2, len(listener2.messages)) 43 | 44 | # now ack only the last message - expecting cumulative behaviour 45 | mid = listener2.messages[1]['headers'][self.ack_id_source_header] 46 | self.ack_message(conn2, mid, None) 47 | finally: 48 | conn2.disconnect() 49 | 50 | # now reconnect again, shouldn't see the message 51 | conn3 = self.create_connection() 52 | try: 53 | listener3 = base.WaitableListener() 54 | conn3.set_listener('', listener3) 55 | self.subscribe_dest(conn3, destination, None) 56 | self.assertFalse(listener3.wait(3), 57 | "unexpected message. ACK not working?") 58 | finally: 59 | conn3.disconnect() 60 | 61 | def test_ack_client_individual(self): 62 | destination = "/queue/ack-test-individual" 63 | 64 | # subscribe and send message 65 | self.listener.reset(2) ## expecting 2 messages 66 | self.subscribe_dest(self.conn, destination, None, 67 | ack='client-individual', 68 | headers={'prefetch-count': '10'}) 69 | self.conn.send(destination, "test1") 70 | self.conn.send(destination, "test2") 71 | self.assertTrue(self.listener.wait(4), "Both initial messages not received") 72 | self.assertEquals(2, len(self.listener.messages)) 73 | 74 | # disconnect without acks 75 | self.conn.disconnect() 76 | 77 | # now reconnect 78 | conn2 = self.create_connection() 79 | try: 80 | listener2 = base.WaitableListener() 81 | listener2.reset(2) ## expect 2 messages 82 | conn2.set_listener('', listener2) 83 | self.subscribe_dest(conn2, destination, None, 84 | ack='client-individual', 85 | headers={'prefetch-count': '10'}) 86 | self.assertTrue(listener2.wait(2.5), "Did not receive 2 messages") 87 | self.assertEquals(2, len(listener2.messages), "Not exactly 2 messages received") 88 | 89 | # now ack only the 'test2' message - expecting individual behaviour 90 | nummsgs = len(listener2.messages) 91 | mid = None 92 | for ind in range(nummsgs): 93 | if listener2.messages[ind]['message']=="test2": 94 | mid = listener2.messages[ind]['headers'][self.ack_id_source_header] 95 | self.assertEquals(1, ind, 'Expecting test2 to be second message') 96 | break 97 | self.assertTrue(mid, "Did not find test2 message id.") 98 | self.ack_message(conn2, mid, None) 99 | finally: 100 | conn2.disconnect() 101 | 102 | # now reconnect again, shouldn't see the message 103 | conn3 = self.create_connection() 104 | try: 105 | listener3 = base.WaitableListener() 106 | listener3.reset(2) ## expecting a single message, but wait for two 107 | conn3.set_listener('', listener3) 108 | self.subscribe_dest(conn3, destination, None) 109 | self.assertFalse(listener3.wait(2.5), 110 | "Expected to see only one message. ACK not working?") 111 | self.assertEquals(1, len(listener3.messages), "Expecting exactly one message") 112 | self.assertEquals("test1", listener3.messages[0]['message'], "Unexpected message remains") 113 | finally: 114 | conn3.disconnect() 115 | 116 | def test_ack_client_tx(self): 117 | destination = "/queue/ack-test-tx" 118 | 119 | # subscribe and send message 120 | self.listener.reset() 121 | self.subscribe_dest(self.conn, destination, None, ack='client') 122 | self.conn.send(destination, "test") 123 | self.assertTrue(self.listener.wait(3), "initial message not received") 124 | self.assertEquals(1, len(self.listener.messages)) 125 | 126 | # disconnect with no ack 127 | self.conn.disconnect() 128 | 129 | # now reconnect 130 | conn2 = self.create_connection() 131 | try: 132 | tx = "abc" 133 | listener2 = base.WaitableListener() 134 | conn2.set_listener('', listener2) 135 | conn2.begin(transaction=tx) 136 | self.subscribe_dest(conn2, destination, None, ack='client') 137 | self.assertTrue(listener2.wait(), "message not received again") 138 | self.assertEquals(1, len(listener2.messages)) 139 | 140 | # now ack 141 | mid = listener2.messages[0]['headers'][self.ack_id_source_header] 142 | self.ack_message(conn2, mid, None, transaction=tx) 143 | 144 | #now commit 145 | conn2.commit(transaction=tx) 146 | finally: 147 | conn2.disconnect() 148 | 149 | # now reconnect again, shouldn't see the message 150 | conn3 = self.create_connection() 151 | try: 152 | listener3 = base.WaitableListener() 153 | conn3.set_listener('', listener3) 154 | self.subscribe_dest(conn3, destination, None) 155 | self.assertFalse(listener3.wait(3), 156 | "unexpected message. TX ACK not working?") 157 | finally: 158 | conn3.disconnect() 159 | 160 | def test_topic_prefetch(self): 161 | destination = "/topic/prefetch-test" 162 | 163 | # subscribe and send message 164 | self.listener.reset(6) ## expect 6 messages 165 | self.subscribe_dest(self.conn, destination, None, 166 | ack='client', 167 | headers={'prefetch-count': '5'}) 168 | 169 | for x in range(10): 170 | self.conn.send(destination, "test" + str(x)) 171 | 172 | self.assertFalse(self.listener.wait(3), 173 | "Should not have been able to see 6 messages") 174 | self.assertEquals(5, len(self.listener.messages)) 175 | 176 | def test_nack(self): 177 | destination = "/queue/nack-test" 178 | 179 | #subscribe and send 180 | self.subscribe_dest(self.conn, destination, None, 181 | ack='client-individual') 182 | self.conn.send(destination, "nack-test") 183 | 184 | self.assertTrue(self.listener.wait(), "Not received message") 185 | message_id = self.listener.messages[0]['headers'][self.ack_id_source_header] 186 | self.listener.reset() 187 | 188 | self.nack_message(self.conn, message_id, None) 189 | self.assertTrue(self.listener.wait(), "Not received message after NACK") 190 | message_id = self.listener.messages[0]['headers'][self.ack_id_source_header] 191 | self.ack_message(self.conn, message_id, None) 192 | 193 | def test_nack_multi(self): 194 | destination = "/queue/nack-multi" 195 | 196 | self.listener.reset(2) 197 | 198 | #subscribe and send 199 | self.subscribe_dest(self.conn, destination, None, 200 | ack='client', 201 | headers = {'prefetch-count' : '10'}) 202 | self.conn.send(destination, "nack-test1") 203 | self.conn.send(destination, "nack-test2") 204 | 205 | self.assertTrue(self.listener.wait(), "Not received messages") 206 | message_id = self.listener.messages[1]['headers'][self.ack_id_source_header] 207 | self.listener.reset(2) 208 | 209 | self.nack_message(self.conn, message_id, None) 210 | self.assertTrue(self.listener.wait(), "Not received message again") 211 | message_id = self.listener.messages[1]['headers'][self.ack_id_source_header] 212 | self.ack_message(self.conn, message_id, None) 213 | 214 | def test_nack_without_requeueing(self): 215 | destination = "/queue/nack-test-no-requeue" 216 | 217 | self.subscribe_dest(self.conn, destination, None, 218 | ack='client-individual') 219 | self.conn.send(destination, "nack-test") 220 | 221 | self.assertTrue(self.listener.wait(), "Not received message") 222 | message_id = self.listener.messages[0]['headers'][self.ack_id_source_header] 223 | self.listener.reset() 224 | 225 | self.conn.send_frame("NACK", {self.ack_id_header: message_id, "requeue": False}) 226 | self.assertFalse(self.listener.wait(4), "Received message after NACK with requeue = False") 227 | 228 | class TestAck11(TestAck): 229 | 230 | def create_connection_obj(self, version='1.1', vhost='/', heartbeats=(0, 0)): 231 | conn = stomp.StompConnection11(host_and_ports=[('localhost', int(os.environ["STOMP_PORT"]))], 232 | vhost=vhost, 233 | heartbeats=heartbeats) 234 | self.ack_id_source_header = 'message-id' 235 | self.ack_id_header = 'message-id' 236 | return conn 237 | 238 | def test_version(self): 239 | self.assertEquals('1.1', self.conn.version) 240 | 241 | class TestAck12(TestAck): 242 | 243 | def create_connection_obj(self, version='1.2', vhost='/', heartbeats=(0, 0)): 244 | conn = stomp.StompConnection12(host_and_ports=[('localhost', int(os.environ["STOMP_PORT"]))], 245 | vhost=vhost, 246 | heartbeats=heartbeats) 247 | self.ack_id_source_header = 'ack' 248 | self.ack_id_header = 'id' 249 | return conn 250 | 251 | def test_version(self): 252 | self.assertEquals('1.2', self.conn.version) 253 | -------------------------------------------------------------------------------- /test/python_SUITE_data/src/amqp_headers.py: -------------------------------------------------------------------------------- 1 | ## This Source Code Form is subject to the terms of the Mozilla Public 2 | ## License, v. 2.0. If a copy of the MPL was not distributed with this 3 | ## file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | ## 5 | ## Copyright (c) 2007-2020 VMware, Inc. or its affiliates. All rights reserved. 6 | ## 7 | 8 | import pika 9 | import base 10 | import os 11 | 12 | class TestAmqpHeaders(base.BaseTest): 13 | def test_headers_to_stomp(self): 14 | self.listener.reset(1) 15 | queueName='test-amqp-headers-to-stomp' 16 | 17 | # Set up STOMP subscription 18 | self.subscribe_dest(self.conn, '/topic/test', None, headers={'x-queue-name': queueName}) 19 | 20 | # Set up AMQP connection 21 | amqp_params = pika.ConnectionParameters(host='localhost', port=int(os.environ["AMQP_PORT"])) 22 | amqp_conn = pika.BlockingConnection(amqp_params) 23 | amqp_chan = amqp_conn.channel() 24 | 25 | # publish a message with headers to the named AMQP queue 26 | amqp_headers = { 'x-custom-hdr-1': 'value1', 27 | 'x-custom-hdr-2': 'value2', 28 | 'custom-hdr-3': 'value3' } 29 | amqp_props = pika.BasicProperties(headers=amqp_headers) 30 | amqp_chan.basic_publish(exchange='', routing_key=queueName, body='Hello World!', properties=amqp_props) 31 | 32 | # check if we receive the message from the STOMP subscription 33 | self.assertTrue(self.listener.wait(2), "initial message not received") 34 | self.assertEquals(1, len(self.listener.messages)) 35 | msg = self.listener.messages[0] 36 | self.assertEquals('Hello World!', msg['message']) 37 | self.assertEquals('value1', msg['headers']['x-custom-hdr-1']) 38 | self.assertEquals('value2', msg['headers']['x-custom-hdr-2']) 39 | self.assertEquals('value3', msg['headers']['custom-hdr-3']) 40 | 41 | self.conn.disconnect() 42 | amqp_conn.close() 43 | -------------------------------------------------------------------------------- /test/python_SUITE_data/src/base.py: -------------------------------------------------------------------------------- 1 | ## This Source Code Form is subject to the terms of the Mozilla Public 2 | ## License, v. 2.0. If a copy of the MPL was not distributed with this 3 | ## file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | ## 5 | ## Copyright (c) 2007-2020 VMware, Inc. or its affiliates. All rights reserved. 6 | ## 7 | 8 | import unittest 9 | import stomp 10 | import sys 11 | import threading 12 | import os 13 | 14 | 15 | class BaseTest(unittest.TestCase): 16 | 17 | def create_connection_obj(self, version='1.0', vhost='/', heartbeats=(0, 0)): 18 | if version == '1.0': 19 | conn = stomp.StompConnection10(host_and_ports=[('localhost', int(os.environ["STOMP_PORT"]))]) 20 | self.ack_id_source_header = 'message-id' 21 | self.ack_id_header = 'message-id' 22 | elif version == '1.1': 23 | conn = stomp.StompConnection11(host_and_ports=[('localhost', int(os.environ["STOMP_PORT"]))], 24 | vhost=vhost, 25 | heartbeats=heartbeats) 26 | self.ack_id_source_header = 'message-id' 27 | self.ack_id_header = 'message-id' 28 | elif version == '1.2': 29 | conn = stomp.StompConnection12(host_and_ports=[('localhost', int(os.environ["STOMP_PORT"]))], 30 | vhost=vhost, 31 | heartbeats=heartbeats) 32 | self.ack_id_source_header = 'ack' 33 | self.ack_id_header = 'id' 34 | else: 35 | conn = stomp.StompConnection12(host_and_ports=[('localhost', int(os.environ["STOMP_PORT"]))], 36 | vhost=vhost, 37 | heartbeats=heartbeats) 38 | conn.version = version 39 | return conn 40 | 41 | def create_connection(self, user='guest', passcode='guest', wait=True, **kwargs): 42 | conn = self.create_connection_obj(**kwargs) 43 | conn.start() 44 | conn.connect(user, passcode, wait=wait) 45 | return conn 46 | 47 | def subscribe_dest(self, conn, destination, sub_id, **kwargs): 48 | if type(conn) is stomp.StompConnection10: 49 | # 'id' is optional in STOMP 1.0. 50 | if sub_id != None: 51 | kwargs['id'] = sub_id 52 | conn.subscribe(destination, **kwargs) 53 | else: 54 | # 'id' is required in STOMP 1.1+. 55 | if sub_id == None: 56 | sub_id = 'ctag' 57 | conn.subscribe(destination, sub_id, **kwargs) 58 | 59 | def unsubscribe_dest(self, conn, destination, sub_id, **kwargs): 60 | if type(conn) is stomp.StompConnection10: 61 | # 'id' is optional in STOMP 1.0. 62 | if sub_id != None: 63 | conn.unsubscribe(id=sub_id, **kwargs) 64 | else: 65 | conn.unsubscribe(destination=destination, **kwargs) 66 | else: 67 | # 'id' is required in STOMP 1.1+. 68 | if sub_id == None: 69 | sub_id = 'ctag' 70 | conn.unsubscribe(sub_id, **kwargs) 71 | 72 | def ack_message(self, conn, msg_id, sub_id, **kwargs): 73 | if type(conn) is stomp.StompConnection10: 74 | conn.ack(msg_id, **kwargs) 75 | elif type(conn) is stomp.StompConnection11: 76 | if sub_id == None: 77 | sub_id = 'ctag' 78 | conn.ack(msg_id, sub_id, **kwargs) 79 | elif type(conn) is stomp.StompConnection12: 80 | conn.ack(msg_id, **kwargs) 81 | 82 | def nack_message(self, conn, msg_id, sub_id, **kwargs): 83 | if type(conn) is stomp.StompConnection10: 84 | # Normally unsupported by STOMP 1.0. 85 | conn.send_frame("NACK", {"message-id": msg_id}) 86 | elif type(conn) is stomp.StompConnection11: 87 | if sub_id == None: 88 | sub_id = 'ctag' 89 | conn.nack(msg_id, sub_id, **kwargs) 90 | elif type(conn) is stomp.StompConnection12: 91 | conn.nack(msg_id, **kwargs) 92 | 93 | def create_subscriber_connection(self, dest): 94 | conn = self.create_connection() 95 | listener = WaitableListener() 96 | conn.set_listener('', listener) 97 | self.subscribe_dest(conn, dest, None, receipt="sub.receipt") 98 | listener.wait() 99 | self.assertEquals(1, len(listener.receipts)) 100 | listener.reset() 101 | return conn, listener 102 | 103 | def setUp(self): 104 | # Note: useful for debugging 105 | # import stomp.listener 106 | self.conn = self.create_connection() 107 | self.listener = WaitableListener() 108 | self.conn.set_listener('waitable', self.listener) 109 | # Note: useful for debugging 110 | # self.printing_listener = stomp.listener.PrintingListener() 111 | # self.conn.set_listener('printing', self.printing_listener) 112 | 113 | def tearDown(self): 114 | if self.conn.is_connected(): 115 | self.conn.disconnect() 116 | self.conn.stop() 117 | 118 | def simple_test_send_rec(self, dest, headers={}): 119 | self.listener.reset() 120 | 121 | self.subscribe_dest(self.conn, dest, None) 122 | self.conn.send(dest, "foo", headers=headers) 123 | 124 | self.assertTrue(self.listener.wait(), "Timeout, no message received") 125 | 126 | # assert no errors 127 | if len(self.listener.errors) > 0: 128 | self.fail(self.listener.errors[0]['message']) 129 | 130 | # check header content 131 | msg = self.listener.messages[0] 132 | self.assertEquals("foo", msg['message']) 133 | self.assertEquals(dest, msg['headers']['destination']) 134 | return msg['headers'] 135 | 136 | def assertListener(self, errMsg, numMsgs=0, numErrs=0, numRcts=0, timeout=10): 137 | if numMsgs + numErrs + numRcts > 0: 138 | self._assertTrue(self.listener.wait(timeout), errMsg + " (#awaiting)") 139 | else: 140 | self._assertFalse(self.listener.wait(timeout), errMsg + " (#awaiting)") 141 | self._assertEquals(numMsgs, len(self.listener.messages), errMsg + " (#messages)") 142 | self._assertEquals(numErrs, len(self.listener.errors), errMsg + " (#errors)") 143 | self._assertEquals(numRcts, len(self.listener.receipts), errMsg + " (#receipts)") 144 | 145 | def _assertTrue(self, bool, msg): 146 | if not bool: 147 | self.listener.print_state(msg, True) 148 | self.assertTrue(bool, msg) 149 | 150 | def _assertFalse(self, bool, msg): 151 | if bool: 152 | self.listener.print_state(msg, True) 153 | self.assertFalse(bool, msg) 154 | 155 | def _assertEquals(self, expected, actual, msg): 156 | if expected != actual: 157 | self.listener.print_state(msg, True) 158 | self.assertEquals(expected, actual, msg) 159 | 160 | def assertListenerAfter(self, verb, errMsg="", numMsgs=0, numErrs=0, numRcts=0, timeout=5): 161 | num = numMsgs + numErrs + numRcts 162 | self.listener.reset(num if num>0 else 1) 163 | verb() 164 | self.assertListener(errMsg=errMsg, numMsgs=numMsgs, numErrs=numErrs, numRcts=numRcts, timeout=timeout) 165 | 166 | class WaitableListener(object): 167 | 168 | def __init__(self): 169 | self.debug = False 170 | if self.debug: 171 | print('(listener) init') 172 | self.messages = [] 173 | self.errors = [] 174 | self.receipts = [] 175 | self.latch = Latch(1) 176 | self.msg_no = 0 177 | 178 | def _next_msg_no(self): 179 | self.msg_no += 1 180 | return self.msg_no 181 | 182 | def _append(self, array, msg, hdrs): 183 | mno = self._next_msg_no() 184 | array.append({'message' : msg, 'headers' : hdrs, 'msg_no' : mno}) 185 | self.latch.countdown() 186 | 187 | def on_receipt(self, headers, message): 188 | if self.debug: 189 | print('(on_receipt) message: {}, headers: {}'.format(message, headers)) 190 | self._append(self.receipts, message, headers) 191 | 192 | def on_error(self, headers, message): 193 | if self.debug: 194 | print('(on_error) message: {}, headers: {}'.format(message, headers)) 195 | self._append(self.errors, message, headers) 196 | 197 | def on_message(self, headers, message): 198 | if self.debug: 199 | print('(on_message) message: {}, headers: {}'.format(message, headers)) 200 | self._append(self.messages, message, headers) 201 | 202 | def reset(self, count=1): 203 | if self.debug: 204 | self.print_state('(reset listener--old state)') 205 | self.messages = [] 206 | self.errors = [] 207 | self.receipts = [] 208 | self.latch = Latch(count) 209 | self.msg_no = 0 210 | if self.debug: 211 | self.print_state('(reset listener--new state)') 212 | 213 | def wait(self, timeout=10): 214 | return self.latch.wait(timeout) 215 | 216 | def print_state(self, hdr="", full=False): 217 | print(hdr) 218 | print('#messages: {}'.format(len(self.messages))) 219 | print('#errors: {}', len(self.errors)) 220 | print('#receipts: {}'.format(len(self.receipts))) 221 | print('Remaining count: {}'.format(self.latch.get_count())) 222 | if full: 223 | if len(self.messages) != 0: print('Messages: {}'.format(self.messages)) 224 | if len(self.errors) != 0: print('Messages: {}'.format(self.errors)) 225 | if len(self.receipts) != 0: print('Messages: {}'.format(self.receipts)) 226 | 227 | class Latch(object): 228 | 229 | def __init__(self, count=1): 230 | self.cond = threading.Condition() 231 | self.cond.acquire() 232 | self.count = count 233 | self.cond.release() 234 | 235 | def countdown(self): 236 | self.cond.acquire() 237 | if self.count > 0: 238 | self.count -= 1 239 | if self.count == 0: 240 | self.cond.notify_all() 241 | self.cond.release() 242 | 243 | def wait(self, timeout=None): 244 | try: 245 | self.cond.acquire() 246 | if self.count == 0: 247 | return True 248 | else: 249 | self.cond.wait(timeout) 250 | return self.count == 0 251 | finally: 252 | self.cond.release() 253 | 254 | def get_count(self): 255 | try: 256 | self.cond.acquire() 257 | return self.count 258 | finally: 259 | self.cond.release() 260 | -------------------------------------------------------------------------------- /test/python_SUITE_data/src/connect_options.py: -------------------------------------------------------------------------------- 1 | ## This Source Code Form is subject to the terms of the Mozilla Public 2 | ## License, v. 2.0. If a copy of the MPL was not distributed with this 3 | ## file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | ## 5 | ## Copyright (c) 2007-2020 VMware, Inc. or its affiliates. All rights reserved. 6 | ## 7 | 8 | import unittest 9 | import stomp 10 | import base 11 | import test_util 12 | import os 13 | 14 | class TestConnectOptions(base.BaseTest): 15 | 16 | def test_implicit_connect(self): 17 | ''' Implicit connect with receipt on first command ''' 18 | self.conn.disconnect() 19 | test_util.enable_implicit_connect() 20 | listener = base.WaitableListener() 21 | new_conn = stomp.Connection(host_and_ports=[('localhost', int(os.environ["STOMP_PORT"]))]) 22 | new_conn.set_listener('', listener) 23 | 24 | new_conn.start() # not going to issue connect 25 | self.subscribe_dest(new_conn, "/topic/implicit", 'sub_implicit', 26 | receipt='implicit') 27 | 28 | try: 29 | self.assertTrue(listener.wait(5)) 30 | self.assertEquals(1, len(listener.receipts), 31 | 'Missing receipt. Likely not connected') 32 | self.assertEquals('implicit', listener.receipts[0]['headers']['receipt-id']) 33 | finally: 34 | new_conn.disconnect() 35 | test_util.disable_implicit_connect() 36 | 37 | def test_default_user(self): 38 | ''' Default user connection ''' 39 | self.conn.disconnect() 40 | test_util.enable_default_user() 41 | listener = base.WaitableListener() 42 | new_conn = stomp.Connection(host_and_ports=[('localhost', int(os.environ["STOMP_PORT"]))]) 43 | new_conn.set_listener('', listener) 44 | new_conn.start() 45 | new_conn.connect() 46 | try: 47 | self.assertFalse(listener.wait(3)) # no error back 48 | self.assertTrue(new_conn.is_connected()) 49 | finally: 50 | new_conn.disconnect() 51 | test_util.disable_default_user() 52 | -------------------------------------------------------------------------------- /test/python_SUITE_data/src/errors.py: -------------------------------------------------------------------------------- 1 | ## This Source Code Form is subject to the terms of the Mozilla Public 2 | ## License, v. 2.0. If a copy of the MPL was not distributed with this 3 | ## file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | ## 5 | ## Copyright (c) 2007-2020 VMware, Inc. or its affiliates. All rights reserved. 6 | ## 7 | 8 | import unittest 9 | import stomp 10 | import base 11 | import time 12 | 13 | class TestErrorsAndCloseConnection(base.BaseTest): 14 | def __test_duplicate_consumer_tag_with_headers(self, destination, headers): 15 | self.subscribe_dest(self.conn, destination, None, 16 | headers = headers) 17 | 18 | self.subscribe_dest(self.conn, destination, None, 19 | headers = headers) 20 | 21 | self.assertTrue(self.listener.wait()) 22 | 23 | self.assertEquals(1, len(self.listener.errors)) 24 | errorReceived = self.listener.errors[0] 25 | self.assertEquals("Duplicated subscription identifier", errorReceived['headers']['message']) 26 | self.assertEquals("A subscription identified by 'T_1' already exists.", errorReceived['message']) 27 | time.sleep(2) 28 | self.assertFalse(self.conn.is_connected()) 29 | 30 | 31 | def test_duplicate_consumer_tag_with_transient_destination(self): 32 | destination = "/exchange/amq.direct/duplicate-consumer-tag-test1" 33 | self.__test_duplicate_consumer_tag_with_headers(destination, {'id': 1}) 34 | 35 | def test_duplicate_consumer_tag_with_durable_destination(self): 36 | destination = "/queue/duplicate-consumer-tag-test2" 37 | self.__test_duplicate_consumer_tag_with_headers(destination, {'id': 1, 38 | 'persistent': True}) 39 | 40 | 41 | class TestErrors(base.BaseTest): 42 | 43 | def test_invalid_queue_destination(self): 44 | self.__test_invalid_destination("queue", "/bah/baz") 45 | 46 | def test_invalid_empty_queue_destination(self): 47 | self.__test_invalid_destination("queue", "") 48 | 49 | def test_invalid_topic_destination(self): 50 | self.__test_invalid_destination("topic", "/bah/baz") 51 | 52 | def test_invalid_empty_topic_destination(self): 53 | self.__test_invalid_destination("topic", "") 54 | 55 | def test_invalid_exchange_destination(self): 56 | self.__test_invalid_destination("exchange", "/bah/baz/boo") 57 | 58 | def test_invalid_empty_exchange_destination(self): 59 | self.__test_invalid_destination("exchange", "") 60 | 61 | def test_invalid_default_exchange_destination(self): 62 | self.__test_invalid_destination("exchange", "//foo") 63 | 64 | def test_unknown_destination(self): 65 | self.listener.reset() 66 | self.conn.send("/something/interesting", 'test_unknown_destination') 67 | 68 | self.assertTrue(self.listener.wait()) 69 | self.assertEquals(1, len(self.listener.errors)) 70 | 71 | err = self.listener.errors[0] 72 | self.assertEquals("Unknown destination", err['headers']['message']) 73 | 74 | def test_send_missing_destination(self): 75 | self.__test_missing_destination("SEND") 76 | 77 | def test_send_missing_destination(self): 78 | self.__test_missing_destination("SUBSCRIBE") 79 | 80 | def __test_missing_destination(self, command): 81 | self.listener.reset() 82 | self.conn.send_frame(command) 83 | 84 | self.assertTrue(self.listener.wait()) 85 | self.assertEquals(1, len(self.listener.errors)) 86 | 87 | err = self.listener.errors[0] 88 | self.assertEquals("Missing destination", err['headers']['message']) 89 | 90 | def __test_invalid_destination(self, dtype, content): 91 | self.listener.reset() 92 | self.conn.send("/" + dtype + content, '__test_invalid_destination:' + dtype + content) 93 | 94 | self.assertTrue(self.listener.wait()) 95 | self.assertEquals(1, len(self.listener.errors)) 96 | 97 | err = self.listener.errors[0] 98 | self.assertEquals("Invalid destination", err['headers']['message']) 99 | self.assertEquals("'" + content + "' is not a valid " + 100 | dtype + " destination\n", 101 | err['message']) 102 | -------------------------------------------------------------------------------- /test/python_SUITE_data/src/lifecycle.py: -------------------------------------------------------------------------------- 1 | ## This Source Code Form is subject to the terms of the Mozilla Public 2 | ## License, v. 2.0. If a copy of the MPL was not distributed with this 3 | ## file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | ## 5 | ## Copyright (c) 2007-2020 VMware, Inc. or its affiliates. All rights reserved. 6 | ## 7 | 8 | import unittest 9 | import stomp 10 | import base 11 | import time 12 | 13 | class TestLifecycle(base.BaseTest): 14 | 15 | def test_unsubscribe_exchange_destination(self): 16 | ''' Test UNSUBSCRIBE command with exchange''' 17 | d = "/exchange/amq.fanout" 18 | self.unsub_test(d, self.sub_and_send(d)) 19 | 20 | def test_unsubscribe_exchange_destination_with_receipt(self): 21 | ''' Test receipted UNSUBSCRIBE command with exchange''' 22 | d = "/exchange/amq.fanout" 23 | self.unsub_test(d, self.sub_and_send(d, receipt="unsub.rct"), numRcts=1) 24 | 25 | def test_unsubscribe_queue_destination(self): 26 | ''' Test UNSUBSCRIBE command with queue''' 27 | d = "/queue/unsub01" 28 | self.unsub_test(d, self.sub_and_send(d)) 29 | 30 | def test_unsubscribe_queue_destination_with_receipt(self): 31 | ''' Test receipted UNSUBSCRIBE command with queue''' 32 | d = "/queue/unsub02" 33 | self.unsub_test(d, self.sub_and_send(d, receipt="unsub.rct"), numRcts=1) 34 | 35 | def test_unsubscribe_exchange_id(self): 36 | ''' Test UNSUBSCRIBE command with exchange by id''' 37 | d = "/exchange/amq.fanout" 38 | self.unsub_test(d, self.sub_and_send(d, subid="exchid")) 39 | 40 | def test_unsubscribe_exchange_id_with_receipt(self): 41 | ''' Test receipted UNSUBSCRIBE command with exchange by id''' 42 | d = "/exchange/amq.fanout" 43 | self.unsub_test(d, self.sub_and_send(d, subid="exchid", receipt="unsub.rct"), numRcts=1) 44 | 45 | def test_unsubscribe_queue_id(self): 46 | ''' Test UNSUBSCRIBE command with queue by id''' 47 | d = "/queue/unsub03" 48 | self.unsub_test(d, self.sub_and_send(d, subid="queid")) 49 | 50 | def test_unsubscribe_queue_id_with_receipt(self): 51 | ''' Test receipted UNSUBSCRIBE command with queue by id''' 52 | d = "/queue/unsub04" 53 | self.unsub_test(d, self.sub_and_send(d, subid="queid", receipt="unsub.rct"), numRcts=1) 54 | 55 | def test_connect_version_1_0(self): 56 | ''' Test CONNECT with version 1.0''' 57 | self.conn.disconnect() 58 | new_conn = self.create_connection(version="1.0") 59 | try: 60 | self.assertTrue(new_conn.is_connected()) 61 | finally: 62 | new_conn.disconnect() 63 | self.assertFalse(new_conn.is_connected()) 64 | 65 | def test_connect_version_1_1(self): 66 | ''' Test CONNECT with version 1.1''' 67 | self.conn.disconnect() 68 | new_conn = self.create_connection(version="1.1") 69 | try: 70 | self.assertTrue(new_conn.is_connected()) 71 | finally: 72 | new_conn.disconnect() 73 | self.assertFalse(new_conn.is_connected()) 74 | 75 | def test_connect_version_1_2(self): 76 | ''' Test CONNECT with version 1.2''' 77 | self.conn.disconnect() 78 | new_conn = self.create_connection(version="1.2") 79 | try: 80 | self.assertTrue(new_conn.is_connected()) 81 | finally: 82 | new_conn.disconnect() 83 | self.assertFalse(new_conn.is_connected()) 84 | 85 | def test_heartbeat_disconnects_client(self): 86 | ''' Test heart-beat disconnection''' 87 | self.conn.disconnect() 88 | new_conn = self.create_connection(version='1.1', heartbeats=(1500, 0)) 89 | try: 90 | self.assertTrue(new_conn.is_connected()) 91 | time.sleep(1) 92 | self.assertTrue(new_conn.is_connected()) 93 | time.sleep(3) 94 | self.assertFalse(new_conn.is_connected()) 95 | finally: 96 | if new_conn.is_connected(): 97 | new_conn.disconnect() 98 | 99 | def test_unsupported_version(self): 100 | ''' Test unsupported version on CONNECT command''' 101 | self.bad_connect("Supported versions are 1.0,1.1,1.2\n", version='100.1') 102 | 103 | def test_bad_username(self): 104 | ''' Test bad username''' 105 | self.bad_connect("Access refused for user 'gust'\n", user='gust') 106 | 107 | def test_bad_password(self): 108 | ''' Test bad password''' 109 | self.bad_connect("Access refused for user 'guest'\n", passcode='gust') 110 | 111 | def test_bad_vhost(self): 112 | ''' Test bad virtual host''' 113 | self.bad_connect("Virtual host '//' access denied", version='1.1', vhost='//') 114 | 115 | def bad_connect(self, expected, user='guest', passcode='guest', **kwargs): 116 | self.conn.disconnect() 117 | new_conn = self.create_connection_obj(**kwargs) 118 | listener = base.WaitableListener() 119 | new_conn.set_listener('', listener) 120 | try: 121 | new_conn.start() 122 | new_conn.connect(user, passcode) 123 | self.assertTrue(listener.wait()) 124 | self.assertEquals(expected, listener.errors[0]['message']) 125 | finally: 126 | if new_conn.is_connected(): 127 | new_conn.disconnect() 128 | 129 | def test_bad_header_on_send(self): 130 | ''' Test disallowed header on SEND ''' 131 | self.listener.reset(1) 132 | self.conn.send_frame("SEND", {"destination":"a", "message-id":"1"}) 133 | self.assertTrue(self.listener.wait()) 134 | self.assertEquals(1, len(self.listener.errors)) 135 | errorReceived = self.listener.errors[0] 136 | self.assertEquals("Invalid header", errorReceived['headers']['message']) 137 | self.assertEquals("'message-id' is not allowed on 'SEND'.\n", errorReceived['message']) 138 | 139 | def test_send_recv_header(self): 140 | ''' Test sending a custom header and receiving it back ''' 141 | dest = '/queue/custom-header' 142 | hdrs = {'x-custom-header-1': 'value1', 143 | 'x-custom-header-2': 'value2', 144 | 'custom-header-3': 'value3'} 145 | self.listener.reset(1) 146 | recv_hdrs = self.simple_test_send_rec(dest, headers=hdrs) 147 | self.assertEquals('value1', recv_hdrs['x-custom-header-1']) 148 | self.assertEquals('value2', recv_hdrs['x-custom-header-2']) 149 | self.assertEquals('value3', recv_hdrs['custom-header-3']) 150 | 151 | def test_disconnect(self): 152 | ''' Test DISCONNECT command''' 153 | self.conn.disconnect() 154 | self.assertFalse(self.conn.is_connected()) 155 | 156 | def test_disconnect_with_receipt(self): 157 | ''' Test the DISCONNECT command with receipts ''' 158 | time.sleep(3) 159 | self.listener.reset(1) 160 | self.conn.send_frame("DISCONNECT", {"receipt": "test"}) 161 | self.assertTrue(self.listener.wait()) 162 | self.assertEquals(1, len(self.listener.receipts)) 163 | receiptReceived = self.listener.receipts[0]['headers']['receipt-id'] 164 | self.assertEquals("test", receiptReceived 165 | , "Wrong receipt received: '" + receiptReceived + "'") 166 | 167 | def unsub_test(self, dest, verbs, numRcts=0): 168 | def afterfun(): 169 | self.conn.send(dest, "after-test") 170 | subverb, unsubverb = verbs 171 | self.assertListenerAfter(subverb, numMsgs=1, 172 | errMsg="FAILED to subscribe and send") 173 | self.assertListenerAfter(unsubverb, numRcts=numRcts, 174 | errMsg="Incorrect responses from UNSUBSCRIBE") 175 | self.assertListenerAfter(afterfun, 176 | errMsg="Still receiving messages") 177 | 178 | def sub_and_send(self, dest, subid=None, receipt=None): 179 | def subfun(): 180 | self.subscribe_dest(self.conn, dest, subid) 181 | self.conn.send(dest, "test") 182 | def unsubfun(): 183 | headers = {} 184 | if receipt != None: 185 | headers['receipt'] = receipt 186 | self.unsubscribe_dest(self.conn, dest, subid, **headers) 187 | return subfun, unsubfun 188 | -------------------------------------------------------------------------------- /test/python_SUITE_data/src/parsing.py: -------------------------------------------------------------------------------- 1 | ## This Source Code Form is subject to the terms of the Mozilla Public 2 | ## License, v. 2.0. If a copy of the MPL was not distributed with this 3 | ## file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | ## 5 | ## Copyright (c) 2007-2020 VMware, Inc. or its affiliates. All rights reserved. 6 | ## 7 | 8 | import unittest 9 | import re 10 | import socket 11 | import functools 12 | import time 13 | import sys 14 | import os 15 | 16 | def connect(cnames): 17 | ''' Decorator that creates stomp connections and issues CONNECT ''' 18 | cmd=('CONNECT\n' 19 | 'login:guest\n' 20 | 'passcode:guest\n' 21 | '\n' 22 | '\n\0') 23 | resp = ('CONNECTED\n' 24 | 'server:RabbitMQ/(.*)\n' 25 | 'session:(.*)\n' 26 | 'heart-beat:0,0\n' 27 | 'version:1.0\n' 28 | '\n\x00') 29 | def w(m): 30 | @functools.wraps(m) 31 | def wrapper(self, *args, **kwargs): 32 | for cname in cnames: 33 | sd = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 34 | sd.settimeout(30000) 35 | sd.connect((self.host, self.port)) 36 | sd.sendall(cmd.encode('utf-8')) 37 | self.match(resp, sd.recv(4096).decode('utf-8')) 38 | setattr(self, cname, sd) 39 | try: 40 | r = m(self, *args, **kwargs) 41 | finally: 42 | for cname in cnames: 43 | try: 44 | getattr(self, cname).close() 45 | except IOError: 46 | pass 47 | return r 48 | return wrapper 49 | return w 50 | 51 | 52 | class TestParsing(unittest.TestCase): 53 | host='127.0.0.1' 54 | # The default port is 61613 but it's in the middle of the ephemeral 55 | # ports range on many operating systems. Therefore, there is a 56 | # chance this port is already in use. Let's use a port close to the 57 | # AMQP default port. 58 | port=int(os.environ["STOMP_PORT"]) 59 | 60 | 61 | def match(self, pattern, data): 62 | ''' helper: try to match a regexp with a string. 63 | Fail test if they do not match. 64 | ''' 65 | matched = re.match(pattern, data) 66 | if matched: 67 | return matched.groups() 68 | self.assertTrue(False, 'No match:\n{}\n\n{}'.format(pattern, data)) 69 | 70 | def recv_atleast(self, bufsize): 71 | recvhead = [] 72 | rl = bufsize 73 | while rl > 0: 74 | buf = self.cd.recv(rl).decode('utf-8') 75 | bl = len(buf) 76 | if bl==0: break 77 | recvhead.append( buf ) 78 | rl -= bl 79 | return ''.join(recvhead) 80 | 81 | 82 | @connect(['cd']) 83 | def test_newline_after_nul(self): 84 | cmd = ('\n' 85 | 'SUBSCRIBE\n' 86 | 'destination:/exchange/amq.fanout\n' 87 | '\n\x00\n' 88 | 'SEND\n' 89 | 'content-type:text/plain\n' 90 | 'destination:/exchange/amq.fanout\n\n' 91 | 'hello\n\x00\n') 92 | self.cd.sendall(cmd.encode('utf-8')) 93 | resp = ('MESSAGE\n' 94 | 'destination:/exchange/amq.fanout\n' 95 | 'message-id:Q_/exchange/amq.fanout@@session-(.*)\n' 96 | 'redelivered:false\n' 97 | 'content-type:text/plain\n' 98 | 'content-length:6\n' 99 | '\n' 100 | 'hello\n\0') 101 | self.match(resp, self.cd.recv(4096).decode('utf-8')) 102 | 103 | @connect(['cd']) 104 | def test_send_without_content_type(self): 105 | cmd = ('\n' 106 | 'SUBSCRIBE\n' 107 | 'destination:/exchange/amq.fanout\n' 108 | '\n\x00\n' 109 | 'SEND\n' 110 | 'destination:/exchange/amq.fanout\n\n' 111 | 'hello\n\x00') 112 | self.cd.sendall(cmd.encode('utf-8')) 113 | resp = ('MESSAGE\n' 114 | 'destination:/exchange/amq.fanout\n' 115 | 'message-id:Q_/exchange/amq.fanout@@session-(.*)\n' 116 | 'redelivered:false\n' 117 | 'content-length:6\n' 118 | '\n' 119 | 'hello\n\0') 120 | self.match(resp, self.cd.recv(4096).decode('utf-8')) 121 | 122 | @connect(['cd']) 123 | def test_send_without_content_type_binary(self): 124 | msg = 'hello' 125 | cmd = ('\n' 126 | 'SUBSCRIBE\n' 127 | 'destination:/exchange/amq.fanout\n' 128 | '\n\x00\n' 129 | 'SEND\n' 130 | 'destination:/exchange/amq.fanout\n' + 131 | 'content-length:{}\n\n'.format(len(msg)) + 132 | '{}\x00'.format(msg)) 133 | self.cd.sendall(cmd.encode('utf-8')) 134 | resp = ('MESSAGE\n' 135 | 'destination:/exchange/amq.fanout\n' 136 | 'message-id:Q_/exchange/amq.fanout@@session-(.*)\n' 137 | 'redelivered:false\n' + 138 | 'content-length:{}\n'.format(len(msg)) + 139 | '\n{}\0'.format(msg)) 140 | self.match(resp, self.cd.recv(4096).decode('utf-8')) 141 | 142 | @connect(['cd']) 143 | def test_newline_after_nul_and_leading_nul(self): 144 | cmd = ('\n' 145 | '\x00SUBSCRIBE\n' 146 | 'destination:/exchange/amq.fanout\n' 147 | '\n\x00\n' 148 | '\x00SEND\n' 149 | 'destination:/exchange/amq.fanout\n' 150 | 'content-type:text/plain\n' 151 | '\nhello\n\x00\n') 152 | self.cd.sendall(cmd.encode('utf-8')) 153 | resp = ('MESSAGE\n' 154 | 'destination:/exchange/amq.fanout\n' 155 | 'message-id:Q_/exchange/amq.fanout@@session-(.*)\n' 156 | 'redelivered:false\n' 157 | 'content-type:text/plain\n' 158 | 'content-length:6\n' 159 | '\n' 160 | 'hello\n\0') 161 | self.match(resp, self.cd.recv(4096).decode('utf-8')) 162 | 163 | @connect(['cd']) 164 | def test_bad_command(self): 165 | ''' Trigger an error message. ''' 166 | cmd = ('WRONGCOMMAND\n' 167 | 'destination:a\n' 168 | 'exchange:amq.fanout\n' 169 | '\n\0') 170 | self.cd.sendall(cmd.encode('utf-8')) 171 | resp = ('ERROR\n' 172 | 'message:Bad command\n' 173 | 'content-type:text/plain\n' 174 | 'version:1.0,1.1,1.2\n' 175 | 'content-length:43\n' 176 | '\n' 177 | 'Could not interpret command "WRONGCOMMAND"\n' 178 | '\0') 179 | self.match(resp, self.cd.recv(4096).decode('utf-8')) 180 | 181 | @connect(['sd', 'cd1', 'cd2']) 182 | def test_broadcast(self): 183 | ''' Single message should be delivered to two consumers: 184 | amq.topic --routing_key--> first_queue --> first_connection 185 | \--routing_key--> second_queue--> second_connection 186 | ''' 187 | subscribe=( 'SUBSCRIBE\n' 188 | 'id: XsKNhAf\n' 189 | 'destination:/exchange/amq.topic/da9d4779\n' 190 | '\n\0') 191 | for cd in [self.cd1, self.cd2]: 192 | cd.sendall(subscribe.encode('utf-8')) 193 | 194 | time.sleep(0.1) 195 | 196 | cmd = ('SEND\n' 197 | 'content-type:text/plain\n' 198 | 'destination:/exchange/amq.topic/da9d4779\n' 199 | '\n' 200 | 'message' 201 | '\n\0') 202 | self.sd.sendall(cmd.encode('utf-8')) 203 | 204 | resp=('MESSAGE\n' 205 | 'subscription:(.*)\n' 206 | 'destination:/topic/da9d4779\n' 207 | 'message-id:(.*)\n' 208 | 'redelivered:false\n' 209 | 'content-type:text/plain\n' 210 | 'content-length:8\n' 211 | '\n' 212 | 'message' 213 | '\n\x00') 214 | for cd in [self.cd1, self.cd2]: 215 | self.match(resp, cd.recv(4096).decode('utf-8')) 216 | 217 | @connect(['cd']) 218 | def test_message_with_embedded_nulls(self): 219 | ''' Test sending/receiving message with embedded nulls. ''' 220 | dest='destination:/exchange/amq.topic/test_embed_nulls_message\n' 221 | resp_dest='destination:/topic/test_embed_nulls_message\n' 222 | subscribe=( 'SUBSCRIBE\n' 223 | 'id:xxx\n' 224 | +dest+ 225 | '\n\0') 226 | self.cd.sendall(subscribe.encode('utf-8')) 227 | 228 | boilerplate = '0123456789'*1024 # large enough boilerplate 229 | message = '01' 230 | oldi = 2 231 | for i in [5, 90, 256-1, 384-1, 512, 1024, 1024+256+64+32]: 232 | message = message + '\0' + boilerplate[oldi+1:i] 233 | oldi = i 234 | msg_len = len(message) 235 | 236 | cmd = ('SEND\n' 237 | +dest+ 238 | 'content-type:text/plain\n' 239 | 'content-length:%i\n' 240 | '\n' 241 | '%s' 242 | '\0' % (len(message), message)) 243 | self.cd.sendall(cmd.encode('utf-8')) 244 | 245 | headresp=('MESSAGE\n' # 8 246 | 'subscription:(.*)\n' # 14 + subscription 247 | +resp_dest+ # 44 248 | 'message-id:(.*)\n' # 12 + message-id 249 | 'redelivered:false\n' # 18 250 | 'content-type:text/plain\n' # 24 251 | 'content-length:%i\n' # 16 + 4==len('1024') 252 | '\n' # 1 253 | '(.*)$' # prefix of body+null (potentially) 254 | % len(message) ) 255 | headlen = 8 + 24 + 14 + (3) + 44 + 12 + 18 + (48) + 16 + (4) + 1 + (1) 256 | 257 | headbuf = self.recv_atleast(headlen) 258 | self.assertFalse(len(headbuf) == 0) 259 | 260 | (sub, msg_id, bodyprefix) = self.match(headresp, headbuf) 261 | bodyresp=( '%s\0' % message ) 262 | bodylen = len(bodyresp); 263 | 264 | bodybuf = ''.join([bodyprefix, 265 | self.recv_atleast(bodylen - len(bodyprefix))]) 266 | 267 | self.assertEqual(len(bodybuf), msg_len+1, 268 | "body received not the same length as message sent") 269 | self.assertEqual(bodybuf, bodyresp, 270 | " body (...'%s')\nincorrectly returned as (...'%s')" 271 | % (bodyresp[-10:], bodybuf[-10:])) 272 | 273 | @connect(['cd']) 274 | def test_message_in_packets(self): 275 | ''' Test sending/receiving message in packets. ''' 276 | base_dest='topic/test_embed_nulls_message\n' 277 | dest='destination:/exchange/amq.' + base_dest 278 | resp_dest='destination:/'+ base_dest 279 | subscribe=( 'SUBSCRIBE\n' 280 | 'id:xxx\n' 281 | +dest+ 282 | '\n\0') 283 | self.cd.sendall(subscribe.encode('utf-8')) 284 | 285 | boilerplate = '0123456789'*1024 # large enough boilerplate 286 | 287 | message = boilerplate[:1024 + 512 + 256 + 32] 288 | msg_len = len(message) 289 | 290 | msg_to_send = ('SEND\n' 291 | +dest+ 292 | 'content-type:text/plain\n' 293 | '\n' 294 | '%s' 295 | '\0' % (message) ) 296 | packet_size = 191 297 | part_index = 0 298 | msg_to_send_len = len(msg_to_send) 299 | while part_index < msg_to_send_len: 300 | part = msg_to_send[part_index:part_index+packet_size] 301 | time.sleep(0.1) 302 | self.cd.sendall(part.encode('utf-8')) 303 | part_index += packet_size 304 | 305 | headresp=('MESSAGE\n' # 8 306 | 'subscription:(.*)\n' # 14 + subscription 307 | +resp_dest+ # 44 308 | 'message-id:(.*)\n' # 12 + message-id 309 | 'redelivered:false\n' # 18 310 | 'content-type:text/plain\n' # 24 311 | 'content-length:%i\n' # 16 + 4==len('1024') 312 | '\n' # 1 313 | '(.*)$' # prefix of body+null (potentially) 314 | % len(message) ) 315 | headlen = 8 + 24 + 14 + (3) + 44 + 12 + 18 + (48) + 16 + (4) + 1 + (1) 316 | 317 | headbuf = self.recv_atleast(headlen) 318 | self.assertFalse(len(headbuf) == 0) 319 | 320 | (sub, msg_id, bodyprefix) = self.match(headresp, headbuf) 321 | bodyresp=( '%s\0' % message ) 322 | bodylen = len(bodyresp); 323 | 324 | bodybuf = ''.join([bodyprefix, 325 | self.recv_atleast(bodylen - len(bodyprefix))]) 326 | 327 | self.assertEqual(len(bodybuf), msg_len+1, 328 | "body received not the same length as message sent") 329 | self.assertEqual(bodybuf, bodyresp, 330 | " body ('%s')\nincorrectly returned as ('%s')" 331 | % (bodyresp, bodybuf)) 332 | -------------------------------------------------------------------------------- /test/python_SUITE_data/src/queue_properties.py: -------------------------------------------------------------------------------- 1 | ## This Source Code Form is subject to the terms of the Mozilla Public 2 | ## License, v. 2.0. If a copy of the MPL was not distributed with this 3 | ## file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | ## 5 | ## Copyright (c) 2007-2020 VMware, Inc. or its affiliates. All rights reserved. 6 | ## 7 | 8 | import unittest 9 | import stomp 10 | import pika 11 | import base 12 | import time 13 | import os 14 | 15 | class TestQueueProperties(base.BaseTest): 16 | 17 | def test_subscribe(self): 18 | destination = "/queue/queue-properties-subscribe-test" 19 | 20 | # subscribe 21 | self.subscribe_dest(self.conn, destination, None, 22 | headers={ 23 | 'x-message-ttl': 60000, 24 | 'x-expires': 70000, 25 | 'x-max-length': 10, 26 | 'x-max-length-bytes': 20000, 27 | 'x-dead-letter-exchange': 'dead-letter-exchange', 28 | 'x-dead-letter-routing-key': 'dead-letter-routing-key', 29 | 'x-max-priority': 6, 30 | }) 31 | 32 | # now try to declare the queue using pika 33 | # if the properties are the same we should 34 | # not get any error 35 | connection = pika.BlockingConnection(pika.ConnectionParameters( 36 | host='127.0.0.1', port=int(os.environ["AMQP_PORT"]))) 37 | channel = connection.channel() 38 | channel.queue_declare(queue='queue-properties-subscribe-test', 39 | durable=True, 40 | arguments={ 41 | 'x-message-ttl': 60000, 42 | 'x-expires': 70000, 43 | 'x-max-length': 10, 44 | 'x-max-length-bytes': 20000, 45 | 'x-dead-letter-exchange': 'dead-letter-exchange', 46 | 'x-dead-letter-routing-key': 'dead-letter-routing-key', 47 | 'x-max-priority': 6, 48 | }) 49 | 50 | self.conn.disconnect() 51 | connection.close() 52 | 53 | def test_send(self): 54 | destination = "/queue/queue-properties-send-test" 55 | 56 | # send 57 | self.conn.send(destination, "test1", 58 | headers={ 59 | 'x-message-ttl': 60000, 60 | 'x-expires': 70000, 61 | 'x-max-length': 10, 62 | 'x-max-length-bytes': 20000, 63 | 'x-dead-letter-exchange': 'dead-letter-exchange', 64 | 'x-dead-letter-routing-key': 'dead-letter-routing-key', 65 | 'x-max-priority': 6, 66 | }) 67 | 68 | # now try to declare the queue using pika 69 | # if the properties are the same we should 70 | # not get any error 71 | connection = pika.BlockingConnection(pika.ConnectionParameters( 72 | host='127.0.0.1', port=int(os.environ["AMQP_PORT"]))) 73 | channel = connection.channel() 74 | channel.queue_declare(queue='queue-properties-send-test', 75 | durable=True, 76 | arguments={ 77 | 'x-message-ttl': 60000, 78 | 'x-expires': 70000, 79 | 'x-max-length': 10, 80 | 'x-max-length-bytes': 20000, 81 | 'x-dead-letter-exchange': 'dead-letter-exchange', 82 | 'x-dead-letter-routing-key': 'dead-letter-routing-key', 83 | 'x-max-priority': 6, 84 | }) 85 | 86 | self.conn.disconnect() 87 | connection.close() 88 | -------------------------------------------------------------------------------- /test/python_SUITE_data/src/redelivered.py: -------------------------------------------------------------------------------- 1 | ## This Source Code Form is subject to the terms of the Mozilla Public 2 | ## License, v. 2.0. If a copy of the MPL was not distributed with this 3 | ## file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | ## 5 | ## Copyright (c) 2007-2020 VMware, Inc. or its affiliates. All rights reserved. 6 | ## 7 | 8 | import unittest 9 | import stomp 10 | import base 11 | import time 12 | 13 | class TestRedelivered(base.BaseTest): 14 | 15 | def test_redelivered(self): 16 | destination = "/queue/redelivered-test" 17 | 18 | # subscribe and send message 19 | self.subscribe_dest(self.conn, destination, None, ack='client') 20 | self.conn.send(destination, "test1") 21 | message_receive_timeout = 30 22 | self.assertTrue(self.listener.wait(message_receive_timeout), "Test message not received within {0} seconds".format(message_receive_timeout)) 23 | self.assertEquals(1, len(self.listener.messages)) 24 | self.assertEquals('false', self.listener.messages[0]['headers']['redelivered']) 25 | 26 | # disconnect with no ack 27 | self.conn.disconnect() 28 | 29 | # now reconnect 30 | conn2 = self.create_connection() 31 | try: 32 | listener2 = base.WaitableListener() 33 | listener2.reset(1) 34 | conn2.set_listener('', listener2) 35 | self.subscribe_dest(conn2, destination, None, ack='client') 36 | self.assertTrue(listener2.wait(), "message not received again") 37 | self.assertEquals(1, len(listener2.messages)) 38 | self.assertEquals('true', listener2.messages[0]['headers']['redelivered']) 39 | finally: 40 | conn2.disconnect() 41 | -------------------------------------------------------------------------------- /test/python_SUITE_data/src/reliability.py: -------------------------------------------------------------------------------- 1 | ## This Source Code Form is subject to the terms of the Mozilla Public 2 | ## License, v. 2.0. If a copy of the MPL was not distributed with this 3 | ## file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | ## 5 | ## Copyright (c) 2007-2020 VMware, Inc. or its affiliates. All rights reserved. 6 | ## 7 | 8 | import base 9 | import stomp 10 | import unittest 11 | import time 12 | 13 | class TestReliability(base.BaseTest): 14 | 15 | def test_send_and_disconnect(self): 16 | ''' Test close socket after send does not lose messages ''' 17 | destination = "/queue/reliability" 18 | pub_conn = self.create_connection() 19 | try: 20 | msg = "0" * (128) 21 | 22 | count = 10000 23 | 24 | listener = base.WaitableListener() 25 | listener.reset(count) 26 | self.conn.set_listener('', listener) 27 | self.subscribe_dest(self.conn, destination, None) 28 | 29 | for x in range(0, count): 30 | pub_conn.send(destination, msg + str(x)) 31 | time.sleep(2.0) 32 | pub_conn.disconnect() 33 | 34 | if listener.wait(30): 35 | self.assertEquals(count, len(listener.messages)) 36 | else: 37 | listener.print_state("Final state of listener:") 38 | self.fail("Did not receive %s messages in time" % count) 39 | finally: 40 | if pub_conn.is_connected(): 41 | pub_conn.disconnect() 42 | -------------------------------------------------------------------------------- /test/python_SUITE_data/src/ssl_lifecycle.py: -------------------------------------------------------------------------------- 1 | ## This Source Code Form is subject to the terms of the Mozilla Public 2 | ## License, v. 2.0. If a copy of the MPL was not distributed with this 3 | ## file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | ## 5 | ## Copyright (c) 2007-2020 VMware, Inc. or its affiliates. All rights reserved. 6 | ## 7 | 8 | import unittest 9 | import os 10 | import os.path 11 | import sys 12 | 13 | import stomp 14 | import base 15 | import ssl 16 | 17 | base_path = os.path.dirname(sys.argv[0]) 18 | 19 | ssl_key_file = os.path.join(os.getenv('SSL_CERTS_PATH'), 'client', 'key.pem') 20 | ssl_cert_file = os.path.join(os.getenv('SSL_CERTS_PATH'), 'client', 'cert.pem') 21 | ssl_ca_certs = os.path.join(os.getenv('SSL_CERTS_PATH'), 'testca', 'cacert.pem') 22 | 23 | class TestSslClient(unittest.TestCase): 24 | 25 | def __ssl_connect(self): 26 | conn = stomp.Connection(host_and_ports = [ ('localhost', int(os.environ["STOMP_PORT_TLS"])) ], 27 | use_ssl = True, ssl_key_file = ssl_key_file, 28 | ssl_cert_file = ssl_cert_file, 29 | ssl_ca_certs = ssl_ca_certs) 30 | print("FILE: ".format(ssl_cert_file)) 31 | conn.start() 32 | conn.connect("guest", "guest") 33 | return conn 34 | 35 | def __ssl_auth_connect(self): 36 | conn = stomp.Connection(host_and_ports = [ ('localhost', int(os.environ["STOMP_PORT_TLS"])) ], 37 | use_ssl = True, ssl_key_file = ssl_key_file, 38 | ssl_cert_file = ssl_cert_file, 39 | ssl_ca_certs = ssl_ca_certs) 40 | conn.start() 41 | conn.connect() 42 | return conn 43 | 44 | def test_ssl_connect(self): 45 | conn = self.__ssl_connect() 46 | conn.disconnect() 47 | 48 | def test_ssl_auth_connect(self): 49 | conn = self.__ssl_auth_connect() 50 | conn.disconnect() 51 | 52 | def test_ssl_send_receive(self): 53 | conn = self.__ssl_connect() 54 | self.__test_conn(conn) 55 | 56 | def test_ssl_auth_send_receive(self): 57 | conn = self.__ssl_auth_connect() 58 | self.__test_conn(conn) 59 | 60 | def __test_conn(self, conn): 61 | try: 62 | listener = base.WaitableListener() 63 | 64 | conn.set_listener('', listener) 65 | 66 | d = "/topic/ssl.test" 67 | conn.subscribe(destination=d, ack="auto", id="ctag", receipt="sub") 68 | 69 | self.assertTrue(listener.wait(1)) 70 | 71 | self.assertEquals("sub", 72 | listener.receipts[0]['headers']['receipt-id']) 73 | 74 | listener.reset(1) 75 | conn.send(body="Hello SSL!", destination=d) 76 | 77 | self.assertTrue(listener.wait()) 78 | 79 | self.assertEquals("Hello SSL!", listener.messages[0]['message']) 80 | finally: 81 | conn.disconnect() 82 | -------------------------------------------------------------------------------- /test/python_SUITE_data/src/test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import test_runner 4 | 5 | if __name__ == '__main__': 6 | modules = [ 7 | 'parsing', 8 | 'errors', 9 | 'lifecycle', 10 | 'ack', 11 | 'amqp_headers', 12 | 'queue_properties', 13 | 'reliability', 14 | 'transactions', 15 | 'x_queue_name', 16 | 'destinations', 17 | 'redelivered', 18 | 'topic_permissions', 19 | 'x_queue_type_quorum' 20 | ] 21 | test_runner.run_unittests(modules) 22 | -------------------------------------------------------------------------------- /test/python_SUITE_data/src/test_connect_options.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | ## This Source Code Form is subject to the terms of the Mozilla Public 4 | ## License, v. 2.0. If a copy of the MPL was not distributed with this 5 | ## file, You can obtain one at https://mozilla.org/MPL/2.0/. 6 | ## 7 | ## Copyright (c) 2007-2020 VMware, Inc. or its affiliates. All rights reserved. 8 | ## 9 | 10 | import test_runner 11 | 12 | if __name__ == '__main__': 13 | modules = ['connect_options'] 14 | test_runner.run_unittests(modules) 15 | 16 | -------------------------------------------------------------------------------- /test/python_SUITE_data/src/test_runner.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | ## This Source Code Form is subject to the terms of the Mozilla Public 4 | ## License, v. 2.0. If a copy of the MPL was not distributed with this 5 | ## file, You can obtain one at https://mozilla.org/MPL/2.0/. 6 | ## 7 | ## Copyright (c) 2007-2020 VMware, Inc. or its affiliates. All rights reserved. 8 | ## 9 | 10 | import unittest 11 | import sys 12 | import os 13 | 14 | def run_unittests(modules): 15 | suite = unittest.TestSuite() 16 | for m in modules: 17 | mod = __import__(m) 18 | for name in dir(mod): 19 | obj = getattr(mod, name) 20 | if name.startswith("Test") and issubclass(obj, unittest.TestCase): 21 | suite.addTest(unittest.TestLoader().loadTestsFromTestCase(obj)) 22 | 23 | ts = unittest.TextTestRunner().run(unittest.TestSuite(suite)) 24 | if ts.errors or ts.failures: 25 | sys.exit(1) 26 | 27 | -------------------------------------------------------------------------------- /test/python_SUITE_data/src/test_ssl.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | ## This Source Code Form is subject to the terms of the Mozilla Public 4 | ## License, v. 2.0. If a copy of the MPL was not distributed with this 5 | ## file, You can obtain one at https://mozilla.org/MPL/2.0/. 6 | ## 7 | ## Copyright (c) 2007-2020 VMware, Inc. or its affiliates. All rights reserved. 8 | ## 9 | 10 | import test_runner 11 | import test_util 12 | 13 | if __name__ == '__main__': 14 | modules = ['ssl_lifecycle'] 15 | test_util.ensure_ssl_auth_user() 16 | test_runner.run_unittests(modules) 17 | 18 | -------------------------------------------------------------------------------- /test/python_SUITE_data/src/test_util.py: -------------------------------------------------------------------------------- 1 | ## This Source Code Form is subject to the terms of the Mozilla Public 2 | ## License, v. 2.0. If a copy of the MPL was not distributed with this 3 | ## file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | ## 5 | ## Copyright (c) 2007-2020 VMware, Inc. or its affiliates. All rights reserved. 6 | ## 7 | 8 | import subprocess 9 | import socket 10 | import sys 11 | import os 12 | import os.path 13 | 14 | def ensure_ssl_auth_user(): 15 | user = 'O=client,CN=%s' % socket.gethostname() 16 | rabbitmqctl(['stop_app']) 17 | rabbitmqctl(['reset']) 18 | rabbitmqctl(['start_app']) 19 | rabbitmqctl(['add_user', user, 'foo']) 20 | rabbitmqctl(['clear_password', user]) 21 | rabbitmqctl(['set_permissions', user, '.*', '.*', '.*']) 22 | 23 | def enable_implicit_connect(): 24 | switch_config(implicit_connect='true', default_user='[{login, "guest"}, {passcode, "guest"}]') 25 | 26 | def disable_implicit_connect(): 27 | switch_config(implicit_connect='false', default_user='[]') 28 | 29 | def enable_default_user(): 30 | switch_config(default_user='[{login, "guest"}, {passcode, "guest"}]') 31 | 32 | def disable_default_user(): 33 | switch_config(default_user='[]') 34 | 35 | def switch_config(implicit_connect='', default_user=''): 36 | cmd = '' 37 | cmd += 'ok = io:format("~n===== Ranch listeners (before stop) =====~n~n~p~n", [ranch:info()]),' 38 | cmd += 'ok = application:stop(rabbitmq_stomp),' 39 | cmd += 'io:format("~n===== Ranch listeners (after stop) =====~n~n~p~n", [ranch:info()]),' 40 | if implicit_connect: 41 | cmd += 'ok = application:set_env(rabbitmq_stomp,implicit_connect,{}),'.format(implicit_connect) 42 | if default_user: 43 | cmd += 'ok = application:set_env(rabbitmq_stomp,default_user,{}),'.format(default_user) 44 | cmd += 'ok = application:start(rabbitmq_stomp),' 45 | cmd += 'io:format("~n===== Ranch listeners (after start) =====~n~n~p~n", [ranch:info()]).' 46 | rabbitmqctl(['eval', cmd]) 47 | 48 | def rabbitmqctl(args): 49 | ctl = os.getenv('RABBITMQCTL') 50 | cmdline = [ctl, '-n', os.getenv('RABBITMQ_NODENAME')] 51 | cmdline.extend(args) 52 | subprocess.check_call(cmdline) 53 | -------------------------------------------------------------------------------- /test/python_SUITE_data/src/topic_permissions.py: -------------------------------------------------------------------------------- 1 | ## This Source Code Form is subject to the terms of the Mozilla Public 2 | ## License, v. 2.0. If a copy of the MPL was not distributed with this 3 | ## file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | ## 5 | ## Copyright (c) 2007-2020 VMware, Inc. or its affiliates. All rights reserved. 6 | ## 7 | 8 | import base 9 | import test_util 10 | import sys 11 | 12 | 13 | class TestTopicPermissions(base.BaseTest): 14 | @classmethod 15 | def setUpClass(cls): 16 | test_util.rabbitmqctl(['set_topic_permissions', 'guest', 'amq.topic', '^{username}.Authorised', '^{username}.Authorised']) 17 | cls.authorised_topic = '/topic/guest.AuthorisedTopic' 18 | cls.restricted_topic = '/topic/guest.RestrictedTopic' 19 | 20 | @classmethod 21 | def tearDownClass(cls): 22 | test_util.rabbitmqctl(['clear_topic_permissions', 'guest']) 23 | 24 | def test_publish_authorisation(self): 25 | ''' Test topic permissions via publish ''' 26 | self.listener.reset() 27 | 28 | # send on authorised topic 29 | self.subscribe_dest(self.conn, self.authorised_topic, None) 30 | self.conn.send(self.authorised_topic, "authorised hello") 31 | 32 | self.assertTrue(self.listener.wait(), "Timeout, no message received") 33 | 34 | # assert no errors 35 | if len(self.listener.errors) > 0: 36 | self.fail(self.listener.errors[0]['message']) 37 | 38 | # check msg content 39 | msg = self.listener.messages[0] 40 | self.assertEqual("authorised hello", msg['message']) 41 | self.assertEqual(self.authorised_topic, msg['headers']['destination']) 42 | 43 | self.listener.reset() 44 | 45 | # send on restricted topic 46 | self.conn.send(self.restricted_topic, "hello") 47 | 48 | self.assertTrue(self.listener.wait(), "Timeout, no message received") 49 | 50 | # assert errors 51 | self.assertGreater(len(self.listener.errors), 0) 52 | self.assertIn("ACCESS_REFUSED", self.listener.errors[0]['message']) 53 | -------------------------------------------------------------------------------- /test/python_SUITE_data/src/transactions.py: -------------------------------------------------------------------------------- 1 | ## This Source Code Form is subject to the terms of the Mozilla Public 2 | ## License, v. 2.0. If a copy of the MPL was not distributed with this 3 | ## file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | ## 5 | ## Copyright (c) 2007-2020 VMware, Inc. or its affiliates. All rights reserved. 6 | ## 7 | 8 | import unittest 9 | import stomp 10 | import base 11 | import time 12 | 13 | class TestTransactions(base.BaseTest): 14 | 15 | def test_tx_commit(self): 16 | ''' Test TX with a COMMIT and ensure messages are delivered ''' 17 | destination = "/exchange/amq.fanout" 18 | tx = "test.tx" 19 | 20 | self.listener.reset() 21 | self.subscribe_dest(self.conn, destination, None) 22 | self.conn.begin(transaction=tx) 23 | self.conn.send(destination, "hello!", transaction=tx) 24 | self.conn.send(destination, "again!") 25 | 26 | ## should see the second message 27 | self.assertTrue(self.listener.wait(3)) 28 | self.assertEquals(1, len(self.listener.messages)) 29 | self.assertEquals("again!", self.listener.messages[0]['message']) 30 | 31 | ## now look for the first message 32 | self.listener.reset() 33 | self.conn.commit(transaction=tx) 34 | self.assertTrue(self.listener.wait(3)) 35 | self.assertEquals(1, len(self.listener.messages), 36 | "Missing committed message") 37 | self.assertEquals("hello!", self.listener.messages[0]['message']) 38 | 39 | def test_tx_abort(self): 40 | ''' Test TX with an ABORT and ensure messages are discarded ''' 41 | destination = "/exchange/amq.fanout" 42 | tx = "test.tx" 43 | 44 | self.listener.reset() 45 | self.subscribe_dest(self.conn, destination, None) 46 | self.conn.begin(transaction=tx) 47 | self.conn.send(destination, "hello!", transaction=tx) 48 | self.conn.send(destination, "again!") 49 | 50 | ## should see the second message 51 | self.assertTrue(self.listener.wait(3)) 52 | self.assertEquals(1, len(self.listener.messages)) 53 | self.assertEquals("again!", self.listener.messages[0]['message']) 54 | 55 | ## now look for the first message to be discarded 56 | self.listener.reset() 57 | self.conn.abort(transaction=tx) 58 | self.assertFalse(self.listener.wait(3)) 59 | self.assertEquals(0, len(self.listener.messages), 60 | "Unexpected committed message") 61 | 62 | -------------------------------------------------------------------------------- /test/python_SUITE_data/src/x_queue_name.py: -------------------------------------------------------------------------------- 1 | ## This Source Code Form is subject to the terms of the Mozilla Public 2 | ## License, v. 2.0. If a copy of the MPL was not distributed with this 3 | ## file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | ## 5 | ## Copyright (c) 2007-2020 VMware, Inc. or its affiliates. All rights reserved. 6 | ## 7 | 8 | import unittest 9 | import stomp 10 | import pika 11 | import base 12 | import time 13 | import os 14 | 15 | class TestUserGeneratedQueueName(base.BaseTest): 16 | 17 | def test_exchange_dest(self): 18 | queueName='my-user-generated-queue-name-exchange' 19 | 20 | # subscribe 21 | self.subscribe_dest( 22 | self.conn, 23 | '/exchange/amq.direct/test', 24 | None, 25 | headers={ 'x-queue-name': queueName } 26 | ) 27 | 28 | connection = pika.BlockingConnection( 29 | pika.ConnectionParameters( host='127.0.0.1', port=int(os.environ["AMQP_PORT"]))) 30 | channel = connection.channel() 31 | 32 | # publish a message to the named queue 33 | channel.basic_publish( 34 | exchange='', 35 | routing_key=queueName, 36 | body='Hello World!') 37 | 38 | # check if we receive the message from the STOMP subscription 39 | self.assertTrue(self.listener.wait(2), "initial message not received") 40 | self.assertEquals(1, len(self.listener.messages)) 41 | 42 | self.conn.disconnect() 43 | connection.close() 44 | 45 | def test_topic_dest(self): 46 | queueName='my-user-generated-queue-name-topic' 47 | 48 | # subscribe 49 | self.subscribe_dest( 50 | self.conn, 51 | '/topic/test', 52 | None, 53 | headers={ 'x-queue-name': queueName } 54 | ) 55 | 56 | connection = pika.BlockingConnection( 57 | pika.ConnectionParameters( host='127.0.0.1', port=int(os.environ["AMQP_PORT"]))) 58 | channel = connection.channel() 59 | 60 | # publish a message to the named queue 61 | channel.basic_publish( 62 | exchange='', 63 | routing_key=queueName, 64 | body='Hello World!') 65 | 66 | # check if we receive the message from the STOMP subscription 67 | self.assertTrue(self.listener.wait(2), "initial message not received") 68 | self.assertEquals(1, len(self.listener.messages)) 69 | 70 | self.conn.disconnect() 71 | connection.close() 72 | -------------------------------------------------------------------------------- /test/python_SUITE_data/src/x_queue_type_quorum.py: -------------------------------------------------------------------------------- 1 | ## This Source Code Form is subject to the terms of the Mozilla Public 2 | ## License, v. 2.0. If a copy of the MPL was not distributed with this 3 | ## file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | ## 5 | ## Copyright (c) 2007-2020 VMware, Inc. or its affiliates. All rights reserved. 6 | ## 7 | 8 | import pika 9 | import base 10 | import time 11 | import os 12 | import re 13 | 14 | 15 | class TestUserGeneratedQueueName(base.BaseTest): 16 | 17 | def test_quorum_queue(self): 18 | queueName = 'my-quorum-queue' 19 | 20 | # subscribe 21 | self.subscribe_dest( 22 | self.conn, 23 | '/topic/quorum-queue-test', 24 | None, 25 | headers={ 26 | 'x-queue-name': queueName, 27 | 'x-queue-type': 'quorum', 28 | 'durable': True, 29 | 'auto-delete': False, 30 | 'id': 1234 31 | } 32 | ) 33 | 34 | # let the quorum queue some time to start 35 | time.sleep(5) 36 | 37 | connection = pika.BlockingConnection( 38 | pika.ConnectionParameters(host='127.0.0.1', port=int(os.environ["AMQP_PORT"]))) 39 | channel = connection.channel() 40 | 41 | # publish a message to the named queue 42 | channel.basic_publish( 43 | exchange='', 44 | routing_key=queueName, 45 | body='Hello World!') 46 | 47 | # could we declare a quorum queue? 48 | quorum_queue_supported = True 49 | if len(self.listener.errors) > 0: 50 | pattern = re.compile(r"feature flag is disabled", re.MULTILINE) 51 | for error in self.listener.errors: 52 | if pattern.search(error['message']) != None: 53 | quorum_queue_supported = False 54 | break 55 | 56 | if quorum_queue_supported: 57 | # check if we receive the message from the STOMP subscription 58 | self.assertTrue(self.listener.wait(5), "initial message not received") 59 | self.assertEquals(1, len(self.listener.messages)) 60 | self.conn.disconnect() 61 | 62 | connection.close() 63 | -------------------------------------------------------------------------------- /test/src/rabbit_stomp_client.erl: -------------------------------------------------------------------------------- 1 | %% This Source Code Form is subject to the terms of the Mozilla Public 2 | %% License, v. 2.0. If a copy of the MPL was not distributed with this 3 | %% file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | %% 5 | %% Copyright (c) 2007-2020 VMware, Inc. or its affiliates. All rights reserved. 6 | %% 7 | 8 | %% The stupidest client imaginable, just for testing. 9 | 10 | -module(rabbit_stomp_client). 11 | 12 | -export([connect/1, connect/2, connect/4, connect/5, disconnect/1, send/2, send/3, send/4, recv/1]). 13 | 14 | -include("rabbit_stomp_frame.hrl"). 15 | 16 | -define(TIMEOUT, 1000). % milliseconds 17 | 18 | connect(Port) -> connect0([], "guest", "guest", Port, []). 19 | connect(V, Port) -> connect0([{"accept-version", V}], "guest", "guest", Port, []). 20 | connect(V, Login, Pass, Port) -> connect0([{"accept-version", V}], Login, Pass, Port, []). 21 | connect(V, Login, Pass, Port, Headers) -> connect0([{"accept-version", V}], Login, Pass, Port, Headers). 22 | 23 | connect0(Version, Login, Pass, Port, Headers) -> 24 | %% The default port is 61613 but it's in the middle of the ephemeral 25 | %% ports range on many operating systems. Therefore, there is a 26 | %% chance this port is already in use. Let's use a port close to the 27 | %% AMQP default port. 28 | {ok, Sock} = gen_tcp:connect(localhost, Port, [{active, false}, binary]), 29 | Client0 = recv_state(Sock), 30 | send(Client0, "CONNECT", [{"login", Login}, 31 | {"passcode", Pass} | Version] ++ Headers), 32 | {#stomp_frame{command = "CONNECTED"}, Client1} = recv(Client0), 33 | {ok, Client1}. 34 | 35 | disconnect(Client = {Sock, _}) -> 36 | send(Client, "DISCONNECT"), 37 | gen_tcp:close(Sock). 38 | 39 | send(Client, Command) -> 40 | send(Client, Command, []). 41 | 42 | send(Client, Command, Headers) -> 43 | send(Client, Command, Headers, []). 44 | 45 | send({Sock, _}, Command, Headers, Body) -> 46 | Frame = rabbit_stomp_frame:serialize( 47 | #stomp_frame{command = list_to_binary(Command), 48 | headers = Headers, 49 | body_iolist = Body}), 50 | gen_tcp:send(Sock, Frame). 51 | 52 | recv_state(Sock) -> 53 | {Sock, []}. 54 | 55 | recv({_Sock, []} = Client) -> 56 | recv(Client, rabbit_stomp_frame:initial_state(), 0); 57 | recv({Sock, [Frame | Frames]}) -> 58 | {Frame, {Sock, Frames}}. 59 | 60 | recv(Client = {Sock, _}, FrameState, Length) -> 61 | {ok, Payload} = gen_tcp:recv(Sock, Length, ?TIMEOUT), 62 | parse(Payload, Client, FrameState, Length). 63 | 64 | parse(Payload, Client = {Sock, FramesRev}, FrameState, Length) -> 65 | case rabbit_stomp_frame:parse(Payload, FrameState) of 66 | {ok, Frame, <<>>} -> 67 | recv({Sock, lists:reverse([Frame | FramesRev])}); 68 | {ok, Frame, <<"\n">>} -> 69 | recv({Sock, lists:reverse([Frame | FramesRev])}); 70 | {ok, Frame, Rest} -> 71 | parse(Rest, {Sock, [Frame | FramesRev]}, 72 | rabbit_stomp_frame:initial_state(), Length); 73 | {more, NewState} -> 74 | recv(Client, NewState, 0) 75 | end. 76 | -------------------------------------------------------------------------------- /test/src/rabbit_stomp_publish_test.erl: -------------------------------------------------------------------------------- 1 | %% This Source Code Form is subject to the terms of the Mozilla Public 2 | %% License, v. 2.0. If a copy of the MPL was not distributed with this 3 | %% file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | %% 5 | %% Copyright (c) 2007-2020 VMware, Inc. or its affiliates. All rights reserved. 6 | %% 7 | 8 | -module(rabbit_stomp_publish_test). 9 | 10 | -export([run/0]). 11 | 12 | -include("rabbit_stomp_frame.hrl"). 13 | 14 | -define(DESTINATION, "/queue/test"). 15 | 16 | -define(MICROS_PER_UPDATE, 5000000). 17 | -define(MICROS_PER_UPDATE_MSG, 100000). 18 | -define(MICROS_PER_SECOND, 1000000). 19 | 20 | %% A very simple publish-and-consume-as-fast-as-you-can test. 21 | 22 | run() -> 23 | [put(K, 0) || K <- [sent, recd, last_sent, last_recd]], 24 | put(last_ts, erlang:monotonic_time()), 25 | {ok, Pub} = rabbit_stomp_client:connect(), 26 | {ok, Recv} = rabbit_stomp_client:connect(), 27 | Self = self(), 28 | spawn(fun() -> publish(Self, Pub, 0, erlang:monotonic_time()) end), 29 | rabbit_stomp_client:send( 30 | Recv, "SUBSCRIBE", [{"destination", ?DESTINATION}]), 31 | spawn(fun() -> recv(Self, Recv, 0, erlang:monotonic_time()) end), 32 | report(). 33 | 34 | report() -> 35 | receive 36 | {sent, C} -> put(sent, C); 37 | {recd, C} -> put(recd, C) 38 | end, 39 | Diff = erlang:convert_time_unit( 40 | erlang:monotonic_time() - get(last_ts), native, microseconds), 41 | case Diff > ?MICROS_PER_UPDATE of 42 | true -> S = get(sent) - get(last_sent), 43 | R = get(recd) - get(last_recd), 44 | put(last_sent, get(sent)), 45 | put(last_recd, get(recd)), 46 | put(last_ts, erlang:monotonic_time()), 47 | io:format("Send ~p msg/s | Recv ~p msg/s~n", 48 | [trunc(S * ?MICROS_PER_SECOND / Diff), 49 | trunc(R * ?MICROS_PER_SECOND / Diff)]); 50 | false -> ok 51 | end, 52 | report(). 53 | 54 | publish(Owner, Client, Count, TS) -> 55 | rabbit_stomp_client:send( 56 | Client, "SEND", [{"destination", ?DESTINATION}], 57 | [integer_to_list(Count)]), 58 | Diff = erlang:convert_time_unit( 59 | erlang:monotonic_time() - TS, native, microseconds), 60 | case Diff > ?MICROS_PER_UPDATE_MSG of 61 | true -> Owner ! {sent, Count + 1}, 62 | publish(Owner, Client, Count + 1, 63 | erlang:monotonic_time()); 64 | false -> publish(Owner, Client, Count + 1, TS) 65 | end. 66 | 67 | recv(Owner, Client0, Count, TS) -> 68 | {#stomp_frame{body_iolist = Body}, Client1} = 69 | rabbit_stomp_client:recv(Client0), 70 | BodyInt = list_to_integer(binary_to_list(iolist_to_binary(Body))), 71 | Count = BodyInt, 72 | Diff = erlang:convert_time_unit( 73 | erlang:monotonic_time() - TS, native, microseconds), 74 | case Diff > ?MICROS_PER_UPDATE_MSG of 75 | true -> Owner ! {recd, Count + 1}, 76 | recv(Owner, Client1, Count + 1, 77 | erlang:monotonic_time()); 78 | false -> recv(Owner, Client1, Count + 1, TS) 79 | end. 80 | 81 | -------------------------------------------------------------------------------- /test/src/test.config: -------------------------------------------------------------------------------- 1 | [{rabbitmq_stomp, [{default_user, []}, 2 | {ssl_cert_login, true}, 3 | {tcp_listeners, [5673]}, 4 | {ssl_listeners, [5674]} 5 | ]}, 6 | {rabbit, [{ssl_options, [{cacertfile,"%%CERTS_DIR%%/testca/cacert.pem"}, 7 | {certfile,"%%CERTS_DIR%%/server/cert.pem"}, 8 | {keyfile,"%%CERTS_DIR%%/server/key.pem"}, 9 | {verify,verify_peer}, 10 | {fail_if_no_peer_cert,true} 11 | ]} 12 | ]} 13 | ]. 14 | -------------------------------------------------------------------------------- /test/topic_SUITE.erl: -------------------------------------------------------------------------------- 1 | %% This Source Code Form is subject to the terms of the Mozilla Public 2 | %% License, v. 2.0. If a copy of the MPL was not distributed with this 3 | %% file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | %% 5 | %% Copyright (c) 2007-2020 VMware, Inc. or its affiliates. All rights reserved. 6 | %% 7 | 8 | -module(topic_SUITE). 9 | 10 | -compile(export_all). 11 | 12 | -include_lib("common_test/include/ct.hrl"). 13 | -include_lib("eunit/include/eunit.hrl"). 14 | -include_lib("amqp_client/include/amqp_client.hrl"). 15 | -include("rabbit_stomp.hrl"). 16 | -include("rabbit_stomp_frame.hrl"). 17 | -include("rabbit_stomp_headers.hrl"). 18 | 19 | all() -> 20 | [{group, list_to_atom("version_" ++ V)} || V <- ?SUPPORTED_VERSIONS]. 21 | 22 | groups() -> 23 | Tests = [ 24 | publish_topic_authorisation, 25 | subscribe_topic_authorisation, 26 | change_default_topic_exchange 27 | ], 28 | 29 | [{list_to_atom("version_" ++ V), [sequence], Tests} 30 | || V <- ?SUPPORTED_VERSIONS]. 31 | 32 | init_per_suite(Config) -> 33 | Config1 = rabbit_ct_helpers:set_config(Config, 34 | [{rmq_nodename_suffix, ?MODULE}]), 35 | rabbit_ct_helpers:log_environment(), 36 | rabbit_ct_helpers:run_setup_steps(Config1, 37 | rabbit_ct_broker_helpers:setup_steps()). 38 | 39 | end_per_suite(Config) -> 40 | rabbit_ct_helpers:run_teardown_steps(Config, 41 | rabbit_ct_broker_helpers:teardown_steps()). 42 | 43 | init_per_group(Group, Config) -> 44 | Version = string:sub_string(atom_to_list(Group), 9), 45 | rabbit_ct_helpers:set_config(Config, [{version, Version}]). 46 | 47 | end_per_group(_Group, Config) -> Config. 48 | 49 | init_per_testcase(_TestCase, Config) -> 50 | Version = ?config(version, Config), 51 | StompPort = rabbit_ct_broker_helpers:get_node_config(Config, 0, tcp_port_stomp), 52 | {ok, Connection} = amqp_connection:start(#amqp_params_direct{ 53 | node = rabbit_ct_broker_helpers:get_node_config(Config, 0, nodename) 54 | }), 55 | {ok, Channel} = amqp_connection:open_channel(Connection), 56 | {ok, Client} = rabbit_stomp_client:connect(Version, StompPort), 57 | Config1 = rabbit_ct_helpers:set_config(Config, [ 58 | {amqp_connection, Connection}, 59 | {amqp_channel, Channel}, 60 | {stomp_client, Client} 61 | ]), 62 | init_per_testcase0(Config1). 63 | 64 | end_per_testcase(_TestCase, Config) -> 65 | Connection = ?config(amqp_connection, Config), 66 | Channel = ?config(amqp_channel, Config), 67 | Client = ?config(stomp_client, Config), 68 | rabbit_stomp_client:disconnect(Client), 69 | amqp_channel:close(Channel), 70 | amqp_connection:close(Connection), 71 | end_per_testcase0(Config). 72 | 73 | init_per_testcase0(Config) -> 74 | rabbit_ct_broker_helpers:rpc(Config, 0, rabbit_auth_backend_internal, add_user, 75 | [<<"user">>, <<"pass">>, <<"acting-user">>]), 76 | rabbit_ct_broker_helpers:rpc(Config, 0, rabbit_auth_backend_internal, set_permissions, [ 77 | <<"user">>, <<"/">>, <<".*">>, <<".*">>, <<".*">>, <<"acting-user">>]), 78 | rabbit_ct_broker_helpers:rpc(Config, 0, rabbit_auth_backend_internal, set_topic_permissions, [ 79 | <<"user">>, <<"/">>, <<"amq.topic">>, <<"^{username}.Authorised">>, <<"^{username}.Authorised">>, <<"acting-user">>]), 80 | Version = ?config(version, Config), 81 | StompPort = rabbit_ct_broker_helpers:get_node_config(Config, 0, tcp_port_stomp), 82 | {ok, ClientFoo} = rabbit_stomp_client:connect(Version, "user", "pass", StompPort), 83 | rabbit_ct_helpers:set_config(Config, [{client_foo, ClientFoo}]). 84 | 85 | end_per_testcase0(Config) -> 86 | ClientFoo = ?config(client_foo, Config), 87 | rabbit_stomp_client:disconnect(ClientFoo), 88 | rabbit_ct_broker_helpers:rpc(Config, 0, rabbit_auth_backend_internal, delete_user, 89 | [<<"user">>, <<"acting-user">>]), 90 | Config. 91 | 92 | publish_topic_authorisation(Config) -> 93 | ClientFoo = ?config(client_foo, Config), 94 | 95 | AuthorisedTopic = "/topic/user.AuthorisedTopic", 96 | RestrictedTopic = "/topic/user.RestrictedTopic", 97 | 98 | %% send on authorised topic 99 | rabbit_stomp_client:send( 100 | ClientFoo, "SUBSCRIBE", [{"destination", AuthorisedTopic}]), 101 | 102 | rabbit_stomp_client:send( 103 | ClientFoo, "SEND", [{"destination", AuthorisedTopic}], ["authorised hello"]), 104 | 105 | {ok, _Client1, _, Body} = stomp_receive(ClientFoo, "MESSAGE"), 106 | [<<"authorised hello">>] = Body, 107 | 108 | %% send on restricted topic 109 | rabbit_stomp_client:send( 110 | ClientFoo, "SEND", [{"destination", RestrictedTopic}], ["hello"]), 111 | {ok, _Client2, Hdrs2, _} = stomp_receive(ClientFoo, "ERROR"), 112 | "access_refused" = proplists:get_value("message", Hdrs2), 113 | ok. 114 | 115 | subscribe_topic_authorisation(Config) -> 116 | ClientFoo = ?config(client_foo, Config), 117 | 118 | AuthorisedTopic = "/topic/user.AuthorisedTopic", 119 | RestrictedTopic = "/topic/user.RestrictedTopic", 120 | 121 | %% subscribe to authorised topic 122 | rabbit_stomp_client:send( 123 | ClientFoo, "SUBSCRIBE", [{"destination", AuthorisedTopic}]), 124 | 125 | rabbit_stomp_client:send( 126 | ClientFoo, "SEND", [{"destination", AuthorisedTopic}], ["authorised hello"]), 127 | 128 | {ok, _Client1, _, Body} = stomp_receive(ClientFoo, "MESSAGE"), 129 | [<<"authorised hello">>] = Body, 130 | 131 | %% subscribe to restricted topic 132 | rabbit_stomp_client:send( 133 | ClientFoo, "SUBSCRIBE", [{"destination", RestrictedTopic}]), 134 | {ok, _Client2, Hdrs2, _} = stomp_receive(ClientFoo, "ERROR"), 135 | "access_refused" = proplists:get_value("message", Hdrs2), 136 | ok. 137 | 138 | change_default_topic_exchange(Config) -> 139 | Channel = ?config(amqp_channel, Config), 140 | ClientFoo = ?config(client_foo, Config), 141 | Ex = <<"my-topic-exchange">>, 142 | AuthorisedTopic = "/topic/user.AuthorisedTopic", 143 | 144 | Declare = #'exchange.declare'{exchange = Ex, type = <<"topic">>}, 145 | #'exchange.declare_ok'{} = amqp_channel:call(Channel, Declare), 146 | 147 | ok = rabbit_ct_broker_helpers:rpc(Config, 0, application, set_env, [rabbitmq_stomp, default_topic_exchange, Ex]), 148 | 149 | rabbit_stomp_client:send( 150 | ClientFoo, "SUBSCRIBE", [{"destination", AuthorisedTopic}]), 151 | 152 | rabbit_stomp_client:send( 153 | ClientFoo, "SEND", [{"destination", AuthorisedTopic}], ["ohai there"]), 154 | 155 | {ok, _Client1, _, Body} = stomp_receive(ClientFoo, "MESSAGE"), 156 | [<<"ohai there">>] = Body, 157 | 158 | Delete = #'exchange.delete'{exchange = Ex}, 159 | #'exchange.delete_ok'{} = amqp_channel:call(Channel, Delete), 160 | ok = rabbit_ct_broker_helpers:rpc(Config, 0, application, unset_env, [rabbitmq_stomp, default_topic_exchange]), 161 | ok. 162 | 163 | 164 | stomp_receive(Client, Command) -> 165 | {#stomp_frame{command = Command, 166 | headers = Hdrs, 167 | body_iolist = Body}, Client1} = 168 | rabbit_stomp_client:recv(Client), 169 | {ok, Client1, Hdrs, Body}. 170 | 171 | -------------------------------------------------------------------------------- /test/util_SUITE.erl: -------------------------------------------------------------------------------- 1 | %% This Source Code Form is subject to the terms of the Mozilla Public 2 | %% License, v. 2.0. If a copy of the MPL was not distributed with this 3 | %% file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | %% 5 | %% Copyright (c) 2007-2020 VMware, Inc. or its affiliates. All rights reserved. 6 | %% 7 | 8 | -module(util_SUITE). 9 | 10 | -include_lib("common_test/include/ct.hrl"). 11 | -include_lib("eunit/include/eunit.hrl"). 12 | -include_lib("amqp_client/include/amqp_client.hrl"). 13 | -include_lib("amqp_client/include/rabbit_routing_prefixes.hrl"). 14 | -include("rabbit_stomp_frame.hrl"). 15 | -compile(export_all). 16 | 17 | all() -> [ 18 | longstr_field, 19 | message_properties, 20 | message_headers, 21 | minimal_message_headers_with_no_custom, 22 | headers_post_process, 23 | headers_post_process_noop_replyto, 24 | headers_post_process_noop2, 25 | negotiate_version_both_empty, 26 | negotiate_version_no_common, 27 | negotiate_version_simple_common, 28 | negotiate_version_two_choice_common, 29 | negotiate_version_two_choice_common_out_of_order, 30 | negotiate_version_two_choice_big_common, 31 | negotiate_version_choice_mismatched_length, 32 | negotiate_version_choice_duplicates, 33 | trim_headers, 34 | ack_mode_auto, 35 | ack_mode_auto_default, 36 | ack_mode_client, 37 | ack_mode_client_individual, 38 | consumer_tag_id, 39 | consumer_tag_destination, 40 | consumer_tag_invalid, 41 | parse_valid_message_id, 42 | parse_invalid_message_id 43 | ]. 44 | 45 | 46 | %%-------------------------------------------------------------------- 47 | %% Header Processing Tests 48 | %%-------------------------------------------------------------------- 49 | 50 | longstr_field(_) -> 51 | {<<"ABC">>, longstr, <<"DEF">>} = 52 | rabbit_stomp_util:longstr_field("ABC", "DEF"). 53 | 54 | message_properties(_) -> 55 | Headers = [ 56 | {"content-type", "text/plain"}, 57 | {"content-encoding", "UTF-8"}, 58 | {"persistent", "true"}, 59 | {"priority", "1"}, 60 | {"correlation-id", "123"}, 61 | {"reply-to", "something"}, 62 | {"expiration", "my-expiration"}, 63 | {"amqp-message-id", "M123"}, 64 | {"timestamp", "123456"}, 65 | {"type", "freshly-squeezed"}, 66 | {"user-id", "joe"}, 67 | {"app-id", "joe's app"}, 68 | {"str", "foo"}, 69 | {"int", "123"} 70 | ], 71 | 72 | #'P_basic'{ 73 | content_type = <<"text/plain">>, 74 | content_encoding = <<"UTF-8">>, 75 | delivery_mode = 2, 76 | priority = 1, 77 | correlation_id = <<"123">>, 78 | reply_to = <<"something">>, 79 | expiration = <<"my-expiration">>, 80 | message_id = <<"M123">>, 81 | timestamp = 123456, 82 | type = <<"freshly-squeezed">>, 83 | user_id = <<"joe">>, 84 | app_id = <<"joe's app">>, 85 | headers = [{<<"str">>, longstr, <<"foo">>}, 86 | {<<"int">>, longstr, <<"123">>}] 87 | } = 88 | rabbit_stomp_util:message_properties(#stomp_frame{headers = Headers}). 89 | 90 | message_headers(_) -> 91 | Properties = #'P_basic'{ 92 | headers = [{<<"str">>, longstr, <<"foo">>}, 93 | {<<"int">>, signedint, 123}], 94 | content_type = <<"text/plain">>, 95 | content_encoding = <<"UTF-8">>, 96 | delivery_mode = 2, 97 | priority = 1, 98 | correlation_id = 123, 99 | reply_to = <<"something">>, 100 | message_id = <<"M123">>, 101 | timestamp = 123456, 102 | type = <<"freshly-squeezed">>, 103 | user_id = <<"joe">>, 104 | app_id = <<"joe's app">>}, 105 | 106 | Headers = rabbit_stomp_util:message_headers(Properties), 107 | 108 | Expected = [ 109 | {"content-type", "text/plain"}, 110 | {"content-encoding", "UTF-8"}, 111 | {"persistent", "true"}, 112 | {"priority", "1"}, 113 | {"correlation-id", "123"}, 114 | {"reply-to", "something"}, 115 | {"expiration", "my-expiration"}, 116 | {"amqp-message-id", "M123"}, 117 | {"timestamp", "123456"}, 118 | {"type", "freshly-squeezed"}, 119 | {"user-id", "joe"}, 120 | {"app-id", "joe's app"}, 121 | {"str", "foo"}, 122 | {"int", "123"} 123 | ], 124 | 125 | [] = lists:subtract(Headers, Expected). 126 | 127 | minimal_message_headers_with_no_custom(_) -> 128 | Properties = #'P_basic'{}, 129 | 130 | Headers = rabbit_stomp_util:message_headers(Properties), 131 | Expected = [ 132 | {"content-type", "text/plain"}, 133 | {"content-encoding", "UTF-8"}, 134 | {"amqp-message-id", "M123"} 135 | ], 136 | 137 | [] = lists:subtract(Headers, Expected). 138 | 139 | headers_post_process(_) -> 140 | Headers = [{"header1", "1"}, 141 | {"header2", "12"}, 142 | {"reply-to", "something"}], 143 | Expected = [{"header1", "1"}, 144 | {"header2", "12"}, 145 | {"reply-to", "/reply-queue/something"}], 146 | [] = lists:subtract( 147 | rabbit_stomp_util:headers_post_process(Headers), Expected). 148 | 149 | headers_post_process_noop_replyto(_) -> 150 | [begin 151 | Headers = [{"reply-to", Prefix ++ "/something"}], 152 | Headers = rabbit_stomp_util:headers_post_process(Headers) 153 | end || Prefix <- rabbit_routing_util:dest_prefixes()]. 154 | 155 | headers_post_process_noop2(_) -> 156 | Headers = [{"header1", "1"}, 157 | {"header2", "12"}], 158 | Expected = [{"header1", "1"}, 159 | {"header2", "12"}], 160 | [] = lists:subtract( 161 | rabbit_stomp_util:headers_post_process(Headers), Expected). 162 | 163 | negotiate_version_both_empty(_) -> 164 | {error, no_common_version} = rabbit_stomp_util:negotiate_version([],[]). 165 | 166 | negotiate_version_no_common(_) -> 167 | {error, no_common_version} = 168 | rabbit_stomp_util:negotiate_version(["1.2"],["1.3"]). 169 | 170 | negotiate_version_simple_common(_) -> 171 | {ok, "1.2"} = 172 | rabbit_stomp_util:negotiate_version(["1.2"],["1.2"]). 173 | 174 | negotiate_version_two_choice_common(_) -> 175 | {ok, "1.3"} = 176 | rabbit_stomp_util:negotiate_version(["1.2", "1.3"],["1.2", "1.3"]). 177 | 178 | negotiate_version_two_choice_common_out_of_order(_) -> 179 | {ok, "1.3"} = 180 | rabbit_stomp_util:negotiate_version(["1.3", "1.2"],["1.2", "1.3"]). 181 | 182 | negotiate_version_two_choice_big_common(_) -> 183 | {ok, "1.20.23"} = 184 | rabbit_stomp_util:negotiate_version(["1.20.23", "1.30.456"], 185 | ["1.20.23", "1.30.457"]). 186 | negotiate_version_choice_mismatched_length(_) -> 187 | {ok, "1.2.3"} = 188 | rabbit_stomp_util:negotiate_version(["1.2", "1.2.3"], 189 | ["1.2.3", "1.2"]). 190 | negotiate_version_choice_duplicates(_) -> 191 | {ok, "1.2"} = 192 | rabbit_stomp_util:negotiate_version(["1.2", "1.2"], 193 | ["1.2", "1.2"]). 194 | trim_headers(_) -> 195 | #stomp_frame{headers = [{"one", "foo"}, {"two", "baz "}]} = 196 | rabbit_stomp_util:trim_headers( 197 | #stomp_frame{headers = [{"one", " foo"}, {"two", " baz "}]}). 198 | 199 | %%-------------------------------------------------------------------- 200 | %% Frame Parsing Tests 201 | %%-------------------------------------------------------------------- 202 | 203 | ack_mode_auto(_) -> 204 | Frame = #stomp_frame{headers = [{"ack", "auto"}]}, 205 | {auto, _} = rabbit_stomp_util:ack_mode(Frame). 206 | 207 | ack_mode_auto_default(_) -> 208 | Frame = #stomp_frame{headers = []}, 209 | {auto, _} = rabbit_stomp_util:ack_mode(Frame). 210 | 211 | ack_mode_client(_) -> 212 | Frame = #stomp_frame{headers = [{"ack", "client"}]}, 213 | {client, true} = rabbit_stomp_util:ack_mode(Frame). 214 | 215 | ack_mode_client_individual(_) -> 216 | Frame = #stomp_frame{headers = [{"ack", "client-individual"}]}, 217 | {client, false} = rabbit_stomp_util:ack_mode(Frame). 218 | 219 | consumer_tag_id(_) -> 220 | Frame = #stomp_frame{headers = [{"id", "foo"}]}, 221 | {ok, <<"T_foo">>, _} = rabbit_stomp_util:consumer_tag(Frame). 222 | 223 | consumer_tag_destination(_) -> 224 | Frame = #stomp_frame{headers = [{"destination", "foo"}]}, 225 | {ok, <<"Q_foo">>, _} = rabbit_stomp_util:consumer_tag(Frame). 226 | 227 | consumer_tag_invalid(_) -> 228 | Frame = #stomp_frame{headers = []}, 229 | {error, missing_destination_header} = rabbit_stomp_util:consumer_tag(Frame). 230 | 231 | %%-------------------------------------------------------------------- 232 | %% Message ID Parsing Tests 233 | %%-------------------------------------------------------------------- 234 | 235 | parse_valid_message_id(_) -> 236 | {ok, {<<"bar">>, "abc", 123}} = 237 | rabbit_stomp_util:parse_message_id("bar@@abc@@123"). 238 | 239 | parse_invalid_message_id(_) -> 240 | {error, invalid_message_id} = 241 | rabbit_stomp_util:parse_message_id("blah"). 242 | 243 | --------------------------------------------------------------------------------