├── LICENSE ├── .gitignore ├── elvis.config ├── src ├── amqp10_client.hrl ├── amqp10_client_sup.erl ├── amqp10_client_app.erl ├── amqp10_client_sessions_sup.erl ├── amqp10_client_connections_sup.erl ├── amqp10_client_connection_sup.erl ├── amqp10_client_types.erl ├── amqp10_client_frame_reader.erl ├── amqp10_client_connection.erl ├── amqp10_msg.erl └── amqp10_client.erl ├── .github ├── ISSUE_TEMPLATE.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ ├── test-erlang-otp-21.3.yaml │ └── test-erlang-otp-22.3.yaml ├── Makefile ├── CODE_OF_CONDUCT.md ├── test ├── mock_server.erl ├── activemq_ct_helpers.erl ├── system_SUITE_data │ └── conf │ │ ├── activemq_no_anon.xml │ │ └── activemq.xml └── msg_SUITE.erl ├── CONTRIBUTING.md ├── README.md ├── LICENSE-MPL-RabbitMQ └── rabbitmq-components.mk /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 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .sw? 2 | .*.sw? 3 | *.beam 4 | *.plt 5 | /.erlang.mk/ 6 | /cover/ 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 | elvis 19 | 20 | amqp10_client.d 21 | /*.coverdata 22 | 23 | # Generated source files. 24 | /include/rabbit_amqp1_0_framing.hrl 25 | /src/rabbit_amqp1_0_framing0.erl 26 | 27 | # Downloaded ActiveMQ. 28 | /test/system_SUITE_data/apache-activemq-* 29 | -------------------------------------------------------------------------------- /elvis.config: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | elvis, 4 | [ 5 | {config, 6 | [#{dirs => ["src"], 7 | filter => "*.erl", 8 | ruleset => erl_files 9 | }, 10 | #{dirs => ["."], 11 | filter => "Makefile", 12 | ruleset => makefiles 13 | }, 14 | #{dirs => ["."], 15 | filter => "rebar.config", 16 | ruleset => rebar_config 17 | }, 18 | #{dirs => ["."], 19 | filter => "elvis.config", 20 | ruleset => elvis_config 21 | } 22 | ] 23 | } 24 | ] 25 | } 26 | ]. 27 | -------------------------------------------------------------------------------- /src/amqp10_client.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(AMQP_PROTOCOL_HEADER, <<"AMQP", 0, 1, 0, 0>>). 9 | -define(SASL_PROTOCOL_HEADER, <<"AMQP", 3, 1, 0, 0>>). 10 | -define(MIN_MAX_FRAME_SIZE, 512). 11 | -define(MAX_MAX_FRAME_SIZE, 1024 * 1024). 12 | -define(FRAME_HEADER_SIZE, 8). 13 | 14 | -define(TIMEOUT, 5000). 15 | 16 | % -define(debug, true). 17 | -ifdef(debug). 18 | -define(DBG(F, A), error_logger:info_msg(F, A)). 19 | -else. 20 | -define(DBG(F, A), ok). 21 | -endif. 22 | 23 | -record(link_ref, {role :: sender | receiver, session :: pid(), 24 | link_handle :: non_neg_integer()}). 25 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | Thank you for using RabbitMQ. 2 | 3 | **STOP NOW AND READ THIS** BEFORE OPENING A NEW ISSUE ON GITHUB 4 | 5 | Unless you are CERTAIN you have found a reproducible problem in RabbitMQ or 6 | have a **specific, actionable** suggestion for our team, you must first ask 7 | your question or discuss your suspected issue on the mailing list: 8 | 9 | https://groups.google.com/forum/#!forum/rabbitmq-users 10 | 11 | Team RabbitMQ does not use GitHub issues for discussions, investigations, root 12 | cause analysis and so on. 13 | 14 | Please take the time to read the CONTRIBUTING.md document for instructions on 15 | how to effectively ask a question or report a suspected issue: 16 | 17 | https://github.com/rabbitmq/rabbitmq-server/blob/master/CONTRIBUTING.md#github-issues 18 | 19 | Following these rules **will save time** for both you and RabbitMQ's maintainers. 20 | 21 | Thank you. 22 | -------------------------------------------------------------------------------- /src/amqp10_client_sup.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 | -module(amqp10_client_sup). 8 | 9 | -behaviour(supervisor). 10 | 11 | %% Private API. 12 | -export([start_link/0]). 13 | 14 | %% Supervisor callbacks. 15 | -export([init/1]). 16 | 17 | -define(CHILD(Id, Mod, Type, Args), {Id, {Mod, start_link, Args}, 18 | temporary, infinity, Type, [Mod]}). 19 | 20 | %% ------------------------------------------------------------------- 21 | %% Private API. 22 | %% ------------------------------------------------------------------- 23 | 24 | start_link() -> 25 | supervisor:start_link({local, ?MODULE}, ?MODULE, []). 26 | 27 | %% ------------------------------------------------------------------- 28 | %% Supervisor callbacks. 29 | %% ------------------------------------------------------------------- 30 | 31 | init([]) -> 32 | Template = ?CHILD(connection_sup, amqp10_client_connection_sup, 33 | supervisor, []), 34 | {ok, {{simple_one_for_one, 0, 1}, [Template]}}. 35 | -------------------------------------------------------------------------------- /src/amqp10_client_app.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(amqp10_client_app). 9 | 10 | -behaviour(application). 11 | 12 | %% Application callbacks 13 | -export([start/2, 14 | stop/1]). 15 | 16 | -type start_type() :: ( 17 | normal | 18 | {takeover, Node :: node()} | 19 | {failover, Node :: node()} 20 | ). 21 | -type state() :: term(). 22 | 23 | %%==================================================================== 24 | %% API 25 | %%==================================================================== 26 | 27 | -spec start(StartType :: start_type(), StartArgs :: term()) -> 28 | {ok, Pid :: pid()} | {ok, Pid :: pid(), State :: state()} | {error, Reason :: term()}. 29 | start(_Type, _Args) -> 30 | amqp10_client_sup:start_link(). 31 | 32 | -spec stop(State :: state()) -> ok. 33 | stop(_State) -> 34 | ok. 35 | 36 | %%==================================================================== 37 | %% Internal functions 38 | %%==================================================================== 39 | -------------------------------------------------------------------------------- /src/amqp10_client_sessions_sup.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 | -module(amqp10_client_sessions_sup). 8 | 9 | -behaviour(supervisor). 10 | 11 | %% Private API. 12 | -export([start_link/0]). 13 | 14 | %% Supervisor callbacks. 15 | -export([init/1]). 16 | 17 | -define(CHILD(Id, Mod, Type, Args), {Id, {Mod, start_link, Args}, 18 | transient, 5000, Type, [Mod]}). 19 | 20 | %% ------------------------------------------------------------------- 21 | %% Private API. 22 | %% ------------------------------------------------------------------- 23 | 24 | -spec start_link() -> 25 | {ok, pid()} | ignore | {error, any()}. 26 | 27 | start_link() -> 28 | supervisor:start_link(?MODULE, []). 29 | 30 | %% ------------------------------------------------------------------- 31 | %% Supervisor callbacks. 32 | %% ------------------------------------------------------------------- 33 | 34 | init(Args) -> 35 | Template = ?CHILD(session, amqp10_client_session, worker, Args), 36 | {ok, {{simple_one_for_one, 0, 1}, [Template]}}. 37 | -------------------------------------------------------------------------------- /src/amqp10_client_connections_sup.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 | -module(amqp10_client_connections_sup). 8 | 9 | -behaviour(supervisor). 10 | 11 | %% Private API. 12 | -export([start_link/0, 13 | stop_child/1]). 14 | 15 | %% Supervisor callbacks. 16 | -export([init/1]). 17 | 18 | -define(CHILD(Id, Mod, Type, Args), {Id, {Mod, start_link, Args}, 19 | temporary, infinity, Type, [Mod]}). 20 | 21 | %% ------------------------------------------------------------------- 22 | %% Private API. 23 | %% ------------------------------------------------------------------- 24 | 25 | stop_child(Pid) -> 26 | supervisor:terminate_child({local, ?MODULE}, Pid). 27 | 28 | start_link() -> 29 | supervisor:start_link({local, ?MODULE}, ?MODULE, []). 30 | 31 | %% ------------------------------------------------------------------- 32 | %% Supervisor callbacks. 33 | %% ------------------------------------------------------------------- 34 | 35 | init([]) -> 36 | Template = ?CHILD(connection_sup, amqp10_client_connection_sup, 37 | supervisor, []), 38 | {ok, {{simple_one_for_one, 0, 1}, [Template]}}. 39 | -------------------------------------------------------------------------------- /src/amqp10_client_connection_sup.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 | -module(amqp10_client_connection_sup). 8 | 9 | -behaviour(supervisor). 10 | 11 | %% Private API. 12 | -export([start_link/1]). 13 | 14 | %% Supervisor callbacks. 15 | -export([init/1]). 16 | 17 | -define(CHILD(Id, Mod, Type, Args), {Id, {Mod, start_link, Args}, 18 | transient, 5000, Type, [Mod]}). 19 | 20 | %% ------------------------------------------------------------------- 21 | %% Private API. 22 | %% ------------------------------------------------------------------- 23 | 24 | -spec start_link(amqp10_client_connection:connection_config()) -> 25 | {ok, pid()} | ignore | {error, any()}. 26 | start_link(Config) -> 27 | supervisor:start_link(?MODULE, [Config]). 28 | 29 | %% ------------------------------------------------------------------- 30 | %% Supervisor callbacks. 31 | %% ------------------------------------------------------------------- 32 | 33 | init(Args) -> 34 | ReaderSpec = ?CHILD(reader, amqp10_client_frame_reader, 35 | worker, [self() | Args]), 36 | ConnectionSpec = ?CHILD(connection, amqp10_client_connection, 37 | worker, [self() | Args]), 38 | SessionsSupSpec = ?CHILD(sessions, amqp10_client_sessions_sup, 39 | supervisor, []), 40 | {ok, {{one_for_all, 0, 1}, [ConnectionSpec, 41 | ReaderSpec, 42 | SessionsSupSpec]}}. 43 | -------------------------------------------------------------------------------- /.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 | - [ ] Bug fix (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 an observable behavior change in existing systems) 22 | - [ ] Documentation improvements (corrections, new content, etc) 23 | - [ ] Cosmetic change (whitespace, formatting, etc) 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 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PROJECT = amqp10_client 2 | PROJECT_DESCRIPTION = AMQP 1.0 client from the RabbitMQ Project 3 | PROJECT_MOD = amqp10_client_app 4 | 5 | BUILD_DEPS = rabbit_common elvis_mk 6 | DEPS = amqp10_common 7 | TEST_DEPS = rabbit rabbitmq_amqp1_0 rabbitmq_ct_helpers 8 | LOCAL_DEPS = ssl inets crypto 9 | 10 | DEP_EARLY_PLUGINS = rabbit_common/mk/rabbitmq-early-test.mk 11 | DEP_PLUGINS = rabbit_common/mk/rabbitmq-macros.mk \ 12 | rabbit_common/mk/rabbitmq-hexpm.mk \ 13 | rabbit_common/mk/rabbitmq-dist.mk \ 14 | rabbit_common/mk/rabbitmq-run.mk \ 15 | rabbit_common/mk/rabbitmq-test.mk \ 16 | rabbit_common/mk/rabbitmq-tools.mk \ 17 | rabbit_common/mk/rabbitmq-github-actions.mk 18 | 19 | DEP_PLUGINS += elvis_mk 20 | dep_elvis_mk = git https://github.com/inaka/elvis.mk.git master 21 | 22 | # FIXME: Use erlang.mk patched for RabbitMQ, while waiting for PRs to be 23 | # reviewed and merged. 24 | 25 | ERLANG_MK_REPO = https://github.com/rabbitmq/erlang.mk.git 26 | ERLANG_MK_COMMIT = rabbitmq-tmp 27 | 28 | include rabbitmq-components.mk 29 | include erlang.mk 30 | 31 | # -------------------------------------------------------------------- 32 | # Compiler flags. 33 | # -------------------------------------------------------------------- 34 | 35 | # gen_fsm is deprecated starting from Erlang 20, but we want to support 36 | # Erlang 19 as well. 37 | 38 | ERTS_VER := $(shell erl -version 2>&1 | sed -E 's/.* version //') 39 | ERLANG_20_ERTS_VER := 9.0 40 | 41 | ifeq ($(call compare_version,$(ERTS_VER),$(ERLANG_20_ERTS_VER),>=),true) 42 | ERLC_OPTS += -Dnowarn_deprecated_gen_fsm 43 | endif 44 | 45 | # Dialyze the tests. 46 | DIALYZER_OPTS += --src -r test 47 | 48 | # -------------------------------------------------------------------- 49 | # ActiveMQ for the testsuite. 50 | # -------------------------------------------------------------------- 51 | 52 | ACTIVEMQ_VERSION := 5.14.4 53 | ACTIVEMQ_URL := 'https://archive.apache.org/dist/activemq/$(ACTIVEMQ_VERSION)/apache-activemq-$(ACTIVEMQ_VERSION)-bin.tar.gz' 54 | 55 | ACTIVEMQ := $(abspath test/system_SUITE_data/apache-activemq-$(ACTIVEMQ_VERSION)/bin/activemq) 56 | export ACTIVEMQ 57 | 58 | $(ACTIVEMQ): \ 59 | test/system_SUITE_data/apache-activemq-$(ACTIVEMQ_VERSION)-bin.tar.gz 60 | $(gen_verbose) cd "$(dir $<)" && tar zxf "$(notdir $<)" 61 | 62 | test/system_SUITE_data/apache-activemq-$(ACTIVEMQ_VERSION)-bin.tar.gz: 63 | $(gen_verbose) $(call core_http_get,$@,$(ACTIVEMQ_URL)) 64 | 65 | tests:: $(ACTIVEMQ) 66 | 67 | ct ct-system: $(ACTIVEMQ) 68 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /test/mock_server.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 | -module(mock_server). 8 | 9 | %% API functions 10 | -export([start/1, 11 | set_steps/2, 12 | stop/1, 13 | run/1, 14 | amqp_step/1, 15 | send_amqp_header_step/1, 16 | recv_amqp_header_step/1 17 | ]). 18 | 19 | -include_lib("src/amqp10_client.hrl"). 20 | 21 | start(Port) -> 22 | {ok, LSock} = gen_tcp:listen(Port, [binary, {packet, 0}, {active, false}]), 23 | Pid = spawn(?MODULE, run, [LSock]), 24 | {LSock, Pid}. 25 | 26 | set_steps({_Sock, Pid}, Steps) -> 27 | Pid ! {set_steps, Steps}, 28 | ok. 29 | 30 | stop({S, P}) -> 31 | P ! close, 32 | gen_tcp:close(S), 33 | exit(P, stop). 34 | 35 | run(Listener) -> 36 | receive 37 | {set_steps, Steps} -> 38 | {ok, Sock} = gen_tcp:accept(Listener), 39 | lists:foreach(fun(S) -> S(Sock) end, Steps), 40 | receive 41 | close -> ok 42 | end 43 | end. 44 | 45 | 46 | send(Socket, Ch, Records) -> 47 | Encoded = [amqp10_framing:encode_bin(R) || R <- Records], 48 | Frame = amqp10_binary_generator:build_frame(Ch, Encoded), 49 | ok = gen_tcp:send(Socket, Frame). 50 | 51 | recv(Sock) -> 52 | {ok, <>} = gen_tcp:recv(Sock, 8), 54 | {ok, Data} = gen_tcp:recv(Sock, Length - 8), 55 | {PerfDesc, Payload} = amqp10_binary_parser:parse(Data), 56 | Perf = amqp10_framing:decode(PerfDesc), 57 | {Ch, Perf, Payload}. 58 | 59 | amqp_step(Fun) -> 60 | fun (Sock) -> 61 | Recv = recv(Sock), 62 | ct:pal("AMQP Step receieved ~p~n", [Recv]), 63 | case Fun(Recv) of 64 | {_Ch, []} -> ok; 65 | {Ch, {multi, Records}} -> 66 | [begin 67 | ct:pal("AMQP multi Step send ~p~n", [R]), 68 | send(Sock, Ch, R) 69 | end || R <- Records]; 70 | {Ch, Records} -> 71 | ct:pal("AMQP Step send ~p~n", [Records]), 72 | send(Sock, Ch, Records) 73 | end 74 | end. 75 | 76 | 77 | send_amqp_header_step(Sock) -> 78 | ct:pal("Sending AMQP protocol header"), 79 | ok = gen_tcp:send(Sock, ?AMQP_PROTOCOL_HEADER). 80 | 81 | recv_amqp_header_step(Sock) -> 82 | ct:pal("Receiving AMQP protocol header"), 83 | {ok, R} = gen_tcp:recv(Sock, 8), 84 | ct:pal("handshake Step receieved ~p~n", [R]). 85 | -------------------------------------------------------------------------------- /src/amqp10_client_types.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 | -module(amqp10_client_types). 8 | 9 | -include_lib("amqp10_common/include/amqp10_framing.hrl"). 10 | 11 | -export([unpack/1, 12 | utf8/1, 13 | uint/1]). 14 | 15 | -type amqp10_performative() :: #'v1_0.open'{} | #'v1_0.begin'{} | #'v1_0.attach'{} | 16 | #'v1_0.flow'{} | #'v1_0.transfer'{} | 17 | #'v1_0.disposition'{} | #'v1_0.detach'{} | 18 | #'v1_0.end'{} | #'v1_0.close'{}. 19 | 20 | -type amqp10_msg_record() :: #'v1_0.transfer'{} | #'v1_0.header'{} | 21 | #'v1_0.delivery_annotations'{} | 22 | #'v1_0.message_annotations'{} | 23 | #'v1_0.properties'{} | 24 | #'v1_0.application_properties'{} | 25 | #'v1_0.data'{} | #'v1_0.amqp_sequence'{} | 26 | #'v1_0.amqp_value'{} | #'v1_0.footer'{}. 27 | 28 | -type channel() :: non_neg_integer(). 29 | 30 | -type source() :: #'v1_0.source'{}. 31 | -type target() :: #'v1_0.target'{}. 32 | 33 | -type delivery_state() :: accepted | rejected | modified | received | released. 34 | 35 | -type amqp_error() :: internal_error | not_found | unauthorized_access | 36 | decode_error | resource_limit_exceeded | 37 | not_allowed | invalid_field | not_implemented | 38 | resource_locked | precondition_failed | resource_deleted | 39 | illegal_state | frame_size_too_small. 40 | 41 | -type connection_error() :: connection_forced | framing_error | redirect. 42 | -type session_error() :: atom(). % TODO 43 | -type link_error() :: atom(). % TODO 44 | 45 | -type connection_event_detail() :: opened | 46 | {closed, Reason::any()} | 47 | {error, {connection_error(), any()}}. 48 | -type session_event_detail() :: begun | ended | {error, {session_error(), any()}}. 49 | -type link_event_detail() :: attached | detached | {error, {link_error(), any()}}. 50 | -type amqp10_event_detail() :: {connection, pid(), connection_event_detail()} | 51 | {session, pid(), session_event_detail()} | 52 | {link, {sender | receiver, Name :: binary()}, 53 | link_event_detail()}. 54 | -type amqp10_event() :: {amqp10_event, amqp10_event_detail()}. 55 | 56 | -export_type([amqp10_performative/0, channel/0, 57 | source/0, target/0, amqp10_msg_record/0, 58 | delivery_state/0, amqp_error/0, connection_error/0, 59 | amqp10_event_detail/0, amqp10_event/0]). 60 | 61 | 62 | unpack(undefined) -> undefined; 63 | unpack({_, Value}) -> Value; 64 | unpack(Value) -> Value. 65 | 66 | utf8(S) when is_list(S) -> {utf8, list_to_binary(S)}; 67 | utf8(B) when is_binary(B) -> {utf8, B}. 68 | 69 | uint(N) -> {uint, N}. 70 | -------------------------------------------------------------------------------- /test/activemq_ct_helpers.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) 2017-2020 VMware, Inc. or its affiliates. All rights reserved. 6 | %% 7 | 8 | -module(activemq_ct_helpers). 9 | 10 | -include_lib("common_test/include/ct.hrl"). 11 | 12 | -export([setup_steps/1, 13 | teardown_steps/0, 14 | ensure_activemq_cmd/1, 15 | init_config_filename/2, 16 | init_tcp_port_numbers/1, 17 | start_activemq_nodes/1, 18 | stop_activemq_nodes/1]). 19 | 20 | setup_steps(ConfigFileName) -> 21 | [fun ensure_activemq_cmd/1, 22 | fun(Config) -> init_config_filename(Config, ConfigFileName) end, 23 | fun init_tcp_port_numbers/1, 24 | fun start_activemq_nodes/1]. 25 | 26 | teardown_steps() -> 27 | [fun stop_activemq_nodes/1]. 28 | 29 | ensure_activemq_cmd(Config) -> 30 | ActivemqCmd= case rabbit_ct_helpers:get_config(Config, activemq_cmd) of 31 | undefined -> 32 | case os:getenv("ACTIVEMQ") of 33 | false -> "activemq"; 34 | M -> M 35 | end; 36 | M -> 37 | M 38 | end, 39 | Cmd = [ActivemqCmd, "--version"], 40 | case rabbit_ct_helpers:exec(Cmd, [{match_stdout, "ActiveMQ"}]) of 41 | {ok, _} -> rabbit_ct_helpers:set_config(Config, 42 | {activemq_cmd, ActivemqCmd}); 43 | _ -> {skip, 44 | "ActiveMQ CLI required, " ++ 45 | "please set ACTIVEMQ or 'activemq_cmd' in ct config"} 46 | end. 47 | 48 | init_config_filename(Config, FileName) -> 49 | ConfigFile = filename:join([?config(data_dir, Config), 50 | "conf", FileName]), 51 | rabbit_ct_helpers:set_config(Config, {activemq_config_filename, ConfigFile}). 52 | 53 | init_tcp_port_numbers(Config) -> 54 | TCPPort = 21000, 55 | NodeConfig = [{nodename, activemq}, 56 | {initial_nodename, activemq}, 57 | {tcp_port_amqp, TCPPort}], 58 | rabbit_ct_helpers:set_config(Config, {rmq_nodes, [NodeConfig]}). 59 | 60 | start_activemq_nodes(Config) -> 61 | Config1 = rabbit_ct_helpers:set_config(Config, 62 | [{rmq_hostname, "localhost"}]), 63 | ActivemqCmd = ?config(activemq_cmd, Config1), 64 | TCPPort = rabbit_ct_broker_helpers:get_node_config(Config1, 0, tcp_port_amqp), 65 | ConfigFile = ?config(activemq_config_filename, Config1), 66 | Cmd = [ActivemqCmd, 67 | "start", 68 | {"-Dtestsuite.tcp_port_amqp=~b", [TCPPort]}, 69 | {"xbean:file:~s", [ConfigFile]}], 70 | case rabbit_ct_helpers:exec(Cmd, []) of 71 | {ok, _} -> wait_for_activemq_nodes(Config1); 72 | Error -> ct:pal("Error: ~p", [Error]), 73 | {skip, "Failed to start ActiveMQ"} 74 | end. 75 | 76 | wait_for_activemq_nodes(Config) -> 77 | Hostname = ?config(rmq_hostname, Config), 78 | Ports = rabbit_ct_broker_helpers:get_node_configs(Config, tcp_port_amqp), 79 | wait_for_activemq_ports(Config, Hostname, Ports). 80 | 81 | wait_for_activemq_ports(Config, Hostname, [Port | Rest]) -> 82 | ct:pal(?LOW_IMPORTANCE, "Waiting for ActiveMQ on port ~b", [Port]), 83 | case wait_for_activemq_port(Hostname, Port, 60) of 84 | ok -> 85 | ct:pal(?LOW_IMPORTANCE, "ActiveMQ ready on port ~b", [Port]), 86 | wait_for_activemq_ports(Config, Hostname, Rest); 87 | {error, _} -> 88 | Msg = lists:flatten( 89 | io_lib:format( 90 | "Failed to start ActiveMQ on port ~b; see ActiveMQ logs", 91 | [Port])), 92 | ct:pal(?LOW_IMPORTANCE, Msg, []), 93 | {skip, Msg} 94 | end; 95 | wait_for_activemq_ports(Config, _, []) -> 96 | Config. 97 | 98 | wait_for_activemq_port(_, _, 0) -> 99 | {error, econnrefused}; 100 | wait_for_activemq_port(Hostname, Port, Retries) -> 101 | case gen_tcp:connect(Hostname, Port, []) of 102 | {ok, Connection} -> 103 | gen_tcp:close(Connection), 104 | ok; 105 | {error, econnrefused} -> 106 | timer:sleep(1000), 107 | wait_for_activemq_port(Hostname, Port, Retries - 1); 108 | Error -> 109 | Error 110 | end. 111 | 112 | stop_activemq_nodes(Config) -> 113 | ActivemqCmd = ?config(activemq_cmd, Config), 114 | Cmd = [ActivemqCmd, "stop"], 115 | case rabbit_ct_helpers:exec(Cmd, []) of 116 | {ok, _} -> Config; 117 | Error -> ct:pal("Error: ~p", [Error]), 118 | {skip, "Failed to stop ActiveMQ"} 119 | end. 120 | -------------------------------------------------------------------------------- /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 | ## Running Tests 80 | 81 | make tests 82 | 83 | will run all suites. 84 | 85 | ## Code of Conduct 86 | 87 | See [CODE_OF_CONDUCT.md](./CODE_OF_CONDUCT.md). 88 | 89 | ## Contributor Agreement 90 | 91 | If you want to contribute a non-trivial change, please submit a signed 92 | copy of our [Contributor Agreement][ca-agreement] around the time you 93 | submit your pull request. This will make it much easier (in some 94 | cases, possible) for the RabbitMQ team at Pivotal to merge your 95 | contribution. 96 | 97 | ## Where to Ask Questions 98 | 99 | If something isn't clear, feel free to ask on our [mailing list][rmq-users]. 100 | 101 | [rmq-collect-env]: https://github.com/rabbitmq/support-tools/blob/master/scripts/rabbitmq-collect-env 102 | [git-commit-msgs]: https://chris.beams.io/posts/git-commit/ 103 | [rmq-users]: https://groups.google.com/forum/#!forum/rabbitmq-users 104 | [ca-agreement]: https://cla.pivotal.io/sign/rabbitmq 105 | [github-fork]: https://help.github.com/articles/fork-a-repo/ 106 | -------------------------------------------------------------------------------- /test/system_SUITE_data/conf/activemq_no_anon.xml: -------------------------------------------------------------------------------- 1 | 17 | 18 | 23 | 24 | 25 | 26 | 27 | file:${activemq.conf}/credentials.properties 28 | 29 | 30 | 31 | 32 | 35 | 36 | 37 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 70 | 71 | 72 | 73 | 74 | 81 | 82 | 83 | 84 | 85 | 86 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 126 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | -------------------------------------------------------------------------------- /test/system_SUITE_data/conf/activemq.xml: -------------------------------------------------------------------------------- 1 | 17 | 18 | 23 | 24 | 25 | 26 | 27 | file:${activemq.conf}/credentials.properties 28 | 29 | 30 | 31 | 32 | 35 | 36 | 37 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 70 | 71 | 72 | 73 | 74 | 81 | 82 | 83 | 84 | 85 | 86 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 126 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Erlang AMQP 1.0 client 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/amqp10_client](https://github.com/rabbitmq/rabbitmq-server/tree/master/deps/amqp10_client). 6 | All issues have been transferred. 7 | 8 | ## Overview 9 | 10 | This is an [Erlang client for the AMQP 1.0](https://www.amqp.org/resources/specifications) protocol. 11 | 12 | It's primary purpose is to be used in RabbitMQ related projects but it is a 13 | generic client that was tested with at least 4 implementations of AMQP 1.0. 14 | 15 | If you are looking for an Erlang client for [AMQP 0-9-1](https://www.rabbitmq.com/tutorials/amqp-concepts.html) — a completely different 16 | protocol despite the name — [consider this one](https://github.com/rabbitmq/rabbitmq-erlang-client). 17 | 18 | ## Project Maturity and Status 19 | 20 | This client is used in the cross-protocol version of the RabbitMQ Shovel plugin. It is not 100% 21 | feature complete but moderately mature and was tested against at least three AMQP 1.0 servers: 22 | RabbitMQ, Azure ServiceBus, ActiveMQ. 23 | 24 | This client library is not officially supported by VMware at this time. 25 | 26 | ## Usage 27 | 28 | ### Connection Settings 29 | 30 | The `connection_config` map contains various configuration properties. 31 | 32 | ``` 33 | -type address :: inet:socket_address() | inet:hostname(). 34 | 35 | -type connection_config() :: 36 | #{container_id => binary(), % mandatory 37 | %% must provide a list of addresses or a single address 38 | addresses => [address()], 39 | address => address(), 40 | %% defaults to 5672, mandatory for TLS 41 | port => inet:port_number(), 42 | % the dns name of the target host 43 | % required by some vendors such as Azure ServiceBus 44 | hostname => binary(), 45 | tls_opts => {secure_port, [ssl:ssl_option()]}, % optional 46 | notify => pid(), % Pid to receive protocol notifications. Set to self() if not provided 47 | max_frame_size => non_neg_integer(), % incoming max frame size 48 | idle_time_out => non_neg_integer(), % heartbeat 49 | sasl => none | anon | {plain, User :: binary(), Password :: binary(), 50 | % set this to a negative value to allow a sender to "overshoot" the flow 51 | % control by this margin 52 | transfer_limit_margin => 0 | neg_integer()} 53 | }. 54 | 55 | ``` 56 | 57 | ### TLS 58 | 59 | TLS is enabled by setting the `tls_opts` connection configuration property. 60 | Currently the only valid value is `{secure_port, [ssl_option]}` where the port 61 | specified only accepts TLS. It is possible that tls negotiation as described 62 | in the amqp 1.0 protocol will be supported in the future. If no value is provided 63 | for `tls_opt` then a plain socket will be used. 64 | 65 | 66 | ### Basic Example 67 | 68 | ``` 69 | %% this will connect to a localhost node 70 | {ok, Hostname} = inet:gethostname(), 71 | User = <<"guest">>, 72 | Password = <<"guest">>, 73 | %% create a configuration map 74 | OpnConf = #{address => Hostname, 75 | port => Port, 76 | container_id => <<"test-container">>, 77 | sasl => {plain, User, Password}}, 78 | {ok, Connection} = amqp10_client:open_connection(OpnConf), 79 | {ok, Session} = amqp10_client:begin_session(Connection), 80 | SenderLinkName = <<"test-sender">>, 81 | {ok, Sender} = amqp10_client:attach_sender_link(Session, SenderLinkName, <<"a-queue-maybe">>), 82 | 83 | %% wait for credit to be received 84 | receive 85 | {amqp10_event, {link, Sender, credited}} -> ok 86 | after 2000 -> 87 | exit(credited_timeout) 88 | end. 89 | 90 | %% create a new message using a delivery-tag, body and indicate 91 | %% it's settlement status (true meaning no disposition confirmation 92 | %% will be sent by the receiver). 93 | OutMsg = amqp10_msg:new(<<"my-tag">>, <<"my-body">>, true), 94 | ok = amqp10_client:send_msg(Sender, OutMsg), 95 | ok = amqp10_client:detach_link(Sender), 96 | 97 | %% create a receiver link 98 | {ok, Receiver} = amqp10_client:attach_receiver_link(Session, <<"test-receiver">>, <<"a-queue-maybe">>), 99 | 100 | %% grant some credit to the remote sender but don't auto-renew it 101 | ok = amqp10_client:flow_link_credit(Receiver, 5, never), 102 | 103 | %% wait for a delivery 104 | receive 105 | {amqp10_msg, Receiver, InMsg} -> ok 106 | after 2000 -> 107 | exit(delivery_timeout) 108 | end. 109 | 110 | ok = amqp10_client:close_connection(Connection), 111 | ``` 112 | 113 | 114 | ### Events 115 | 116 | The `ampq10_client` API is mostly asynchronous with respect to the AMQP 1.0 117 | protocol. Functions such as `amqp10_client:open_connection` typically return 118 | after the `Open` frame has been successfully written to the socket rather than 119 | waiting until the remote end returns with their `Open` frame. The client will 120 | notify the caller of various internal/async events using `amqp10_event` 121 | messages. In the example above when the remote replies with their `Open` frame 122 | a message is sent of the following forma: 123 | 124 | ``` 125 | {amqp10_event, {connection, ConnectionPid, opened}} 126 | ``` 127 | 128 | When the connection is closed an event is issued as such: 129 | 130 | ``` 131 | {amqp10_event, {connection, ConnectionPid, {closed, Why}}} 132 | ``` 133 | 134 | `Why` could be `normal` or contain a description of an error that occured 135 | and resulted in the closure of the connection. 136 | 137 | Likewise sessions and links have similar events using a similar format. 138 | 139 | ``` 140 | %% success events 141 | {amqp10_event, {connection, ConnectionPid, opened}} 142 | {amqp10_event, {session, SessionPid, begun}} 143 | {amqp10_event, {link, LinkRef, attached}} 144 | ``` 145 | 146 | ``` 147 | %% error events 148 | {amqp10_event, {connection, ConnectionPid, {closed, Why}}} 149 | {amqp10_event, {session, SessionPid, {ended, Why}}} 150 | {amqp10_event, {link, LinkRef, {detached, Why}}} 151 | ``` 152 | 153 | In addition the client may notify the initiator of certain protocol 154 | events such as a receiver running out of credit or credit being available 155 | to a sender. 156 | 157 | ``` 158 | %% no more credit available to sender 159 | {amqp10_event, {link, Sender, credit_exhausted}} 160 | %% sender credit received 161 | {amqp10_event, {link, Sender, credited}} 162 | ``` 163 | 164 | Other events may be declared as necessary, Hence it makes sense for a user 165 | of the client to handle all `{amqp10_event, _}` events to ensure unexpected 166 | messages aren't kept around in the mailbox. 167 | -------------------------------------------------------------------------------- /test/msg_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) 2017-2020 VMware, Inc. or its affiliates. All rights reserved. 6 | %% 7 | 8 | -module(msg_SUITE). 9 | 10 | -include_lib("common_test/include/ct.hrl"). 11 | -include_lib("eunit/include/eunit.hrl"). 12 | 13 | -include_lib("amqp10_common/include/amqp10_framing.hrl"). 14 | 15 | -compile(export_all). 16 | 17 | 18 | all() -> 19 | [{group, tests}]. 20 | 21 | groups() -> 22 | [ 23 | {tests, [parallel], [ 24 | minimal_input, 25 | amqp_bodies, 26 | full_input, 27 | new, 28 | new_with_options, 29 | new_amqp_value, 30 | new_amqp_sequence, 31 | set_message_format, 32 | set_headers, 33 | update_headers, 34 | set_properties, 35 | set_application_properties, 36 | set_delivery_annotations, 37 | set_message_annotations, 38 | to_amqp_records, 39 | set_handle, 40 | body_bin_data, 41 | body_bin_amqp_value, 42 | body_bin_amqp_sequence 43 | 44 | ]} 45 | ]. 46 | 47 | %% ------------------------------------------------------------------- 48 | %% Testsuite setup/teardown. 49 | %% ------------------------------------------------------------------- 50 | 51 | init_per_suite(Config) -> Config. 52 | 53 | end_per_suite(Config) -> Config. 54 | 55 | %% ------------------------------------------------------------------- 56 | %% Groups. 57 | %% ------------------------------------------------------------------- 58 | 59 | init_per_group(_, Config) -> Config. 60 | 61 | end_per_group(_, Config) -> Config. 62 | 63 | minimal_input(_Config) -> 64 | Tag = <<"tag">>, 65 | Content = <<"content">>, 66 | Input = [#'v1_0.transfer'{delivery_tag = {utf8, Tag}, 67 | delivery_id = {uint, 5672}}, 68 | #'v1_0.data'{content = Content}], 69 | Res = amqp10_msg:from_amqp_records(Input), 70 | Tag = amqp10_msg:delivery_tag(Res), 71 | 5672 = amqp10_msg:delivery_id(Res), 72 | undefined = amqp10_msg:message_format(Res), 73 | #{} = amqp10_msg:headers(Res), 74 | #{} = amqp10_msg:delivery_annotations(Res), 75 | #{} = amqp10_msg:message_annotations(Res), 76 | #{} = amqp10_msg:properties(Res), 77 | #{} = amqp10_msg:application_properties(Res), 78 | false = amqp10_msg:header(durable, Res), 79 | 4 = amqp10_msg:header(priority, Res), 80 | false = amqp10_msg:header(first_acquirer, Res), 81 | 0 = amqp10_msg:header(delivery_count, Res), 82 | undefined = amqp10_msg:header(ttl, Res). 83 | 84 | amqp_bodies(_Config) -> 85 | Tag = <<"tag">>, 86 | Content = <<"hi">>, 87 | Data = #'v1_0.data'{content = Content}, 88 | Value = #'v1_0.amqp_value'{content = utf8("hi")}, 89 | Seq = #'v1_0.amqp_sequence'{content = {list, [utf8("hi"), utf8("there")]}}, 90 | Transfer = #'v1_0.transfer'{delivery_tag = {utf8, Tag}}, 91 | 92 | Res1 = amqp10_msg:from_amqp_records([Transfer, Data]), 93 | [<<"hi">>] = amqp10_msg:body(Res1), 94 | 95 | Res2 = amqp10_msg:from_amqp_records([Transfer, Value]), 96 | #'v1_0.amqp_value'{content = {utf8, <<"hi">>}} = amqp10_msg:body(Res2), 97 | 98 | Res3 = amqp10_msg:from_amqp_records([Transfer, Seq]), 99 | [#'v1_0.amqp_sequence'{content = {list, [{utf8, <<"hi">>}, 100 | {utf8, <<"there">>} 101 | ]}}] = amqp10_msg:body(Res3). 102 | 103 | full_input(_Config) -> 104 | Tag = <<"tag">>, 105 | Content = <<"content">>, 106 | %% Format / Version 107 | <> = <<101:24/unsigned, 2:8/unsigned>>, 108 | Input = [#'v1_0.transfer'{delivery_tag = utf8("tag"), 109 | message_format = {uint, MessageFormat} 110 | }, 111 | #'v1_0.header'{durable = true, priority = 9, ttl = 1004, 112 | first_acquirer = true, delivery_count = 101}, 113 | #'v1_0.delivery_annotations'{content = 114 | [{utf8("key"), utf8("value")} 115 | ]}, 116 | #'v1_0.message_annotations'{content = 117 | [{utf8("key"), utf8("value")} 118 | ]}, 119 | #'v1_0.properties'{message_id = utf8("msg-id"), 120 | user_id = utf8("zen"), 121 | to = utf8("to"), 122 | subject = utf8("subject"), 123 | reply_to = utf8("reply-to"), 124 | correlation_id = utf8("correlation_id"), 125 | content_type = {symbol, <<"utf8">>}, 126 | content_encoding = {symbol, <<"gzip">>}, 127 | absolute_expiry_time = {timestamp, 1000}, 128 | creation_time = {timestamp, 10}, 129 | group_id = utf8("group-id"), 130 | group_sequence = {uint, 33}, 131 | reply_to_group_id = utf8("reply-to-group-id") 132 | }, 133 | #'v1_0.application_properties'{content = 134 | [{utf8("key"), utf8("value")}]}, 135 | #'v1_0.data'{content = Content}, 136 | #'v1_0.footer'{content = [{utf8("key"), utf8("value")}]} 137 | ], 138 | Res = amqp10_msg:from_amqp_records(Input), 139 | Tag = amqp10_msg:delivery_tag(Res), 140 | {101, 2} = amqp10_msg:message_format(Res), 141 | Headers = amqp10_msg:headers(Res), 142 | #{durable := true, 143 | priority := 9, 144 | first_acquirer := true, 145 | delivery_count := 101, 146 | ttl := 1004} = Headers, 147 | 148 | % header/2 149 | true = amqp10_msg:header(durable, Res), 150 | 9 = amqp10_msg:header(priority, Res), 151 | true = amqp10_msg:header(first_acquirer, Res), 152 | 101 = amqp10_msg:header(delivery_count, Res), 153 | 1004 = amqp10_msg:header(ttl, Res), % no default 154 | 155 | #{<<"key">> := <<"value">>} = amqp10_msg:delivery_annotations(Res), 156 | #{<<"key">> := <<"value">>} = amqp10_msg:message_annotations(Res), 157 | #{message_id := <<"msg-id">>, 158 | user_id := <<"zen">>, 159 | to := <<"to">>, 160 | subject := <<"subject">>, 161 | reply_to := <<"reply-to">>, 162 | correlation_id := <<"correlation_id">>, 163 | content_type := <<"utf8">>, 164 | content_encoding := <<"gzip">>, 165 | absolute_expiry_time := 1000, 166 | creation_time := 10, 167 | group_id := <<"group-id">>, 168 | group_sequence := 33, 169 | reply_to_group_id := <<"reply-to-group-id">>} = amqp10_msg:properties(Res), 170 | 171 | #{<<"key">> := <<"value">>} = amqp10_msg:application_properties(Res), 172 | 173 | ?assertEqual([Content], amqp10_msg:body(Res)), 174 | 175 | #{<<"key">> := <<"value">>} = amqp10_msg:footer(Res). 176 | 177 | new(_Config) -> 178 | Tag = <<"tag">>, 179 | Body = <<"hi">>, 180 | Msg = amqp10_msg:new(Tag, Body), 181 | Tag = amqp10_msg:delivery_tag(Msg), 182 | [<<"hi">>] = amqp10_msg:body(Msg). 183 | 184 | new_with_options(_Config) -> 185 | Tag = <<"tag">>, 186 | Body = <<"hi">>, 187 | Msg = amqp10_msg:new(Tag, Body, true), 188 | Tag = amqp10_msg:delivery_tag(Msg), 189 | true = amqp10_msg:settled(Msg). 190 | 191 | new_amqp_value(_Config) -> 192 | Tag = <<"tag">>, 193 | Body = #'v1_0.amqp_value'{content = {utf8, <<"hi">>}}, 194 | Msg = amqp10_msg:new(Tag, Body), 195 | Tag = amqp10_msg:delivery_tag(Msg), 196 | Body = amqp10_msg:body(Msg). 197 | 198 | new_amqp_sequence(_Config) -> 199 | Tag = <<"tag">>, 200 | Body = #'v1_0.amqp_sequence'{content = {list, [utf8("hi"), utf8("there")]}}, 201 | Msg = amqp10_msg:new(Tag, [Body]), 202 | Tag = amqp10_msg:delivery_tag(Msg), 203 | [Body] = amqp10_msg:body(Msg). 204 | 205 | set_message_format(_Config) -> 206 | MsgFormat = {103, 3}, 207 | Msg0 = amqp10_msg:new(<<"tag">>, <<"hi">>), 208 | Msg1 = amqp10_msg:set_message_format(MsgFormat, Msg0), 209 | MsgFormat = amqp10_msg:message_format(Msg1). 210 | 211 | set_headers(_Config) -> 212 | Headers = #{durable => true}, 213 | Msg0 = amqp10_msg:new(<<"tag">>, <<"hi">>), 214 | Msg1 = amqp10_msg:set_headers(Headers, Msg0), 215 | #{durable := true} = amqp10_msg:headers(Msg1). 216 | 217 | update_headers(_Config) -> 218 | Headers = #{priority => 5}, 219 | Msg0 = amqp10_msg:new(<<"tag">>, <<"hi">>), 220 | Msg1 = amqp10_msg:set_headers(Headers, Msg0), 221 | #{priority := 5} = amqp10_msg:headers(Msg1), 222 | Msg2 = amqp10_msg:set_headers(Headers#{priority => 9}, Msg1), 223 | #{priority := 9} = amqp10_msg:headers(Msg2). 224 | 225 | set_handle(_Config) -> 226 | Msg0 = amqp10_msg:new(<<"tag">>, <<"hi">>), 227 | Msg1 = amqp10_msg:set_handle(42, Msg0), 228 | 42 = amqp10_msg:handle(Msg1). 229 | 230 | set_properties(_Config) -> 231 | Props = #{message_id => <<"msg-id">>, 232 | user_id => <<"zen">>, 233 | to => <<"to">>, 234 | subject => <<"subject">>, 235 | reply_to => <<"reply-to">>, 236 | correlation_id => <<"correlation_id">>, 237 | content_type => <<"utf8">>, 238 | content_encoding => <<"gzip">>, 239 | absolute_expiry_time => 1000, 240 | creation_time => 10, 241 | group_id => <<"group-id">>, 242 | group_sequence => 33, 243 | reply_to_group_id => <<"reply-to-group-id">>}, 244 | 245 | Msg0 = amqp10_msg:new(<<"tag">>, <<"hi">>), 246 | Msg1 = amqp10_msg:set_properties(Props, Msg0), 247 | Props = amqp10_msg:properties(Msg1). 248 | 249 | set_application_properties(_Config) -> 250 | Props = #{"key" => "value", 251 | <<"key2">> => <<"value2">>}, 252 | Msg0 = amqp10_msg:new(<<"tag">>, <<"hi">>), 253 | Msg1 = amqp10_msg:set_application_properties(Props, Msg0), 254 | #{<<"key">> := <<"value">>, 255 | <<"key2">> := <<"value2">>} = amqp10_msg:application_properties(Msg1), 256 | ok. 257 | 258 | set_delivery_annotations(_Config) -> 259 | Props = #{<<"x-key">> => "value", 260 | <<"x-key2">> => 9}, 261 | Msg0 = amqp10_msg:new(<<"tag">>, <<"hi">>), 262 | Msg1 = amqp10_msg:set_delivery_annotations(Props, Msg0), 263 | #{<<"x-key">> := <<"value">>, 264 | <<"x-key2">> := 9} = amqp10_msg:delivery_annotations(Msg1). 265 | 266 | set_message_annotations(_Config) -> 267 | Props = #{<<"x-key">> => "value", 268 | <<"x-key2">> => 9}, 269 | Msg0 = amqp10_msg:new(<<"tag">>, <<"hi">>), 270 | Msg1 = amqp10_msg:set_message_annotations(Props, Msg0), 271 | #{<<"x-key">> := <<"value">>, 272 | <<"x-key2">> := 9} = amqp10_msg:message_annotations(Msg1). 273 | 274 | to_amqp_records(_Config) -> 275 | Msg0 = amqp10_msg:new(<<"tag">>, <<"data">>), 276 | Msg = amqp10_msg:set_headers(#{durable => true}, Msg0), 277 | [#'v1_0.transfer'{}, 278 | #'v1_0.header'{durable = true}, 279 | #'v1_0.data'{content = <<"data">>}] = 280 | amqp10_msg:to_amqp_records(Msg). 281 | 282 | body_bin_data(_) -> 283 | Body = [ 284 | #'v1_0.data'{content = <<"one">>}, 285 | #'v1_0.data'{content = <<"two">>} 286 | ], 287 | Msg = amqp10_msg:new(<<55>>, Body), 288 | Bin = amqp10_msg:body_bin(Msg), 289 | Body = amqp10_framing:decode_bin(Bin), 290 | ok. 291 | 292 | body_bin_amqp_value(_) -> 293 | Body = #'v1_0.amqp_value'{content = {utf8, <<"one">>}}, 294 | Msg = amqp10_msg:new(<<55>>, Body), 295 | Bin = amqp10_msg:body_bin(Msg), 296 | [Body] = amqp10_framing:decode_bin(Bin), 297 | ok. 298 | 299 | body_bin_amqp_sequence(_) -> 300 | Body = [ 301 | #'v1_0.amqp_sequence'{content = [{utf8, <<"one">>}, 302 | {utf8, <<"blah">>}]}, 303 | #'v1_0.amqp_sequence'{content = [{utf8, <<"two">>}]} 304 | ], 305 | Msg = amqp10_msg:new(<<55>>, Body), 306 | Bin = amqp10_msg:body_bin(Msg), 307 | Body = amqp10_framing:decode_bin(Bin), 308 | ok. 309 | 310 | %% ------------------------------------------------------------------- 311 | %% Utilities 312 | %% ------------------------------------------------------------------- 313 | 314 | utf8(S) -> amqp10_client_types:utf8(S). 315 | -------------------------------------------------------------------------------- /src/amqp10_client_frame_reader.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 | -module(amqp10_client_frame_reader). 8 | 9 | -behaviour(gen_statem). 10 | 11 | -include("amqp10_client.hrl"). 12 | -include_lib("amqp10_common/include/amqp10_framing.hrl"). 13 | 14 | -ifdef(TEST). 15 | -include_lib("eunit/include/eunit.hrl"). 16 | -endif. 17 | 18 | %% API 19 | -export([start_link/2, 20 | set_connection/2, 21 | close/1, 22 | register_session/3, 23 | unregister_session/4]). 24 | 25 | %% gen_statem callbacks 26 | -export([init/1, 27 | callback_mode/0, 28 | handle_event/4, 29 | code_change/4, 30 | terminate/3]). 31 | 32 | -define(RABBIT_TCP_OPTS, [binary, 33 | {packet, 0}, 34 | {active, false}, 35 | {nodelay, true}]). 36 | 37 | -type frame_type() :: amqp | sasl. 38 | 39 | -record(frame_state, 40 | {data_offset :: 2..255, 41 | type :: frame_type(), 42 | channel :: non_neg_integer(), 43 | frame_length :: pos_integer()}). 44 | 45 | -record(state, 46 | {connection_sup :: pid(), 47 | socket :: amqp10_client_connection:amqp10_socket() | undefined, 48 | buffer = <<>> :: binary(), 49 | frame_state :: #frame_state{} | undefined, 50 | connection :: pid() | undefined, 51 | heartbeat_timer_ref :: reference() | undefined, 52 | connection_config = #{} :: amqp10_client_connection:connection_config(), 53 | outgoing_channels = #{}, 54 | incoming_channels = #{}}). 55 | 56 | %%%=================================================================== 57 | %%% API 58 | %%%=================================================================== 59 | 60 | -spec start_link(pid(), amqp10_client_connection:connection_config()) -> 61 | {ok, pid()} | ignore | {error, any()}. 62 | start_link(Sup, Config) -> 63 | gen_statem:start_link(?MODULE, [Sup, Config], []). 64 | 65 | %% @private 66 | %% @doc 67 | %% Passes the connection process PID to the reader process. 68 | %% 69 | %% This function is called when a connection supervision tree is 70 | %% started. 71 | -spec set_connection(Reader :: pid(), ConnectionPid :: pid()) -> ok. 72 | set_connection(Reader, Connection) -> 73 | gen_statem:cast(Reader, {set_connection, Connection}). 74 | 75 | close(Reader) -> 76 | gen_statem:cast(Reader, close). 77 | 78 | register_session(Reader, Session, OutgoingChannel) -> 79 | gen_statem:cast(Reader, {register_session, Session, OutgoingChannel}). 80 | 81 | unregister_session(Reader, Session, OutgoingChannel, IncomingChannel) -> 82 | gen_statem:cast(Reader, {unregister_session, Session, OutgoingChannel, IncomingChannel}). 83 | 84 | %%%=================================================================== 85 | %%% gen_statem callbacks 86 | %%%=================================================================== 87 | 88 | callback_mode() -> 89 | [handle_event_function]. 90 | 91 | init([Sup, ConnConfig]) when is_map(ConnConfig) -> 92 | Port = maps:get(port, ConnConfig, 5672), 93 | %% combined the list of `addresses' with the value of the original `address' option if provided 94 | Addresses0 = maps:get(addresses, ConnConfig, []), 95 | Addresses = case maps:get(address, ConnConfig, undefined) of 96 | undefined -> Addresses0; 97 | Address -> Addresses0 ++ [Address] 98 | end, 99 | Result = lists:foldl(fun (Address, {error, _}) -> 100 | gen_tcp:connect(Address, Port, ?RABBIT_TCP_OPTS); 101 | (_Address, {ok, Socket}) -> 102 | {ok, Socket} 103 | end, 104 | {error, undefined}, Addresses), 105 | case Result of 106 | {ok, Socket0} -> 107 | Socket = case ConnConfig of 108 | #{tls_opts := {secure_port, Opts}} -> 109 | {ok, SslSock} = ssl:connect(Socket0, Opts), 110 | {ssl, SslSock}; 111 | _ -> {tcp, Socket0} 112 | end, 113 | State = #state{connection_sup = Sup, socket = Socket, 114 | connection_config = ConnConfig}, 115 | {ok, expecting_connection_pid, State}; 116 | {error, Reason} -> 117 | {stop, Reason} 118 | end. 119 | 120 | handle_event(cast, {set_connection, ConnectionPid}, expecting_connection_pid, 121 | State=#state{socket = Socket}) -> 122 | ok = amqp10_client_connection:socket_ready(ConnectionPid, Socket), 123 | set_active_once(State), 124 | State1 = State#state{connection = ConnectionPid}, 125 | {next_state, expecting_frame_header, State1}; 126 | handle_event(cast, {register_session, Session, OutgoingChannel}, _StateName, 127 | #state{socket = Socket, outgoing_channels = OutgoingChannels} = State) -> 128 | ok = amqp10_client_session:socket_ready(Session, Socket), 129 | OutgoingChannels1 = OutgoingChannels#{OutgoingChannel => Session}, 130 | State1 = State#state{outgoing_channels = OutgoingChannels1}, 131 | {keep_state, State1}; 132 | handle_event(cast, {unregister_session, _Session, OutgoingChannel, IncomingChannel}, _StateName, 133 | State=#state{outgoing_channels = OutgoingChannels, 134 | incoming_channels = IncomingChannels}) -> 135 | OutgoingChannels1 = maps:remove(OutgoingChannel, OutgoingChannels), 136 | IncomingChannels1 = maps:remove(IncomingChannel, IncomingChannels), 137 | State1 = State#state{outgoing_channels = OutgoingChannels1, 138 | incoming_channels = IncomingChannels1}, 139 | {keep_state, State1}; 140 | handle_event(cast, close, _StateName, State = #state{socket = Socket}) -> 141 | close_socket(Socket), 142 | {stop, normal, State#state{socket = undefined}}; 143 | 144 | handle_event({call, From}, _Action, _State, _Data) -> 145 | {keep_state_and_data, [{reply, From, ok}]}; 146 | 147 | handle_event(info, {Tcp, _, Packet}, StateName, #state{buffer = Buffer} = State) 148 | when Tcp == tcp orelse Tcp == ssl -> 149 | Data = <>, 150 | case handle_input(StateName, Data, State) of 151 | {ok, NextState, Remaining, NewState0} -> 152 | NewState = defer_heartbeat_timer(NewState0), 153 | set_active_once(NewState), 154 | {next_state, NextState, NewState#state{buffer = Remaining}}; 155 | {error, Reason, NewState} -> 156 | {stop, Reason, NewState} 157 | end; 158 | 159 | handle_event(info, {TcpError, _, Reason}, StateName, State) 160 | when TcpError == tcp_error orelse TcpError == ssl_error -> 161 | error_logger:warning_msg("AMQP 1.0 connection socket errored, connection state: '~s', reason: '~p'~n", 162 | [StateName, Reason]), 163 | State1 = State#state{socket = undefined, 164 | buffer = <<>>, 165 | frame_state = undefined}, 166 | {stop, {error, Reason}, State1}; 167 | handle_event(info, {TcpClosed, _}, StateName, State) 168 | when TcpClosed == tcp_closed orelse TcpClosed == ssl_closed -> 169 | error_logger:warning_msg("AMQP 1.0 connection socket was closed, connection state: '~s'~n", 170 | [StateName]), 171 | State1 = State#state{socket = undefined, 172 | buffer = <<>>, 173 | frame_state = undefined}, 174 | {stop, normal, State1}; 175 | 176 | handle_event(info, heartbeat, _StateName, #state{connection = Connection}) -> 177 | amqp10_client_connection:close(Connection, 178 | {resource_limit_exceeded, <<"remote idle-time-out">>}), 179 | % do not stop as may want to read the peer's close frame 180 | keep_state_and_data. 181 | 182 | terminate(normal, _StateName, #state{connection_sup = _Sup, socket = Socket}) -> 183 | maybe_close_socket(Socket); 184 | terminate(_Reason, _StateName, #state{connection_sup = _Sup, socket = Socket}) -> 185 | maybe_close_socket(Socket). 186 | 187 | code_change(_Vsn, State, Data, _Extra) -> 188 | {ok, State, Data}. 189 | 190 | %%%=================================================================== 191 | %%% Internal functions 192 | %%%=================================================================== 193 | 194 | maybe_close_socket(undefined) -> 195 | ok; 196 | maybe_close_socket(Socket) -> 197 | close_socket(Socket). 198 | 199 | close_socket({tcp, Socket}) -> 200 | gen_tcp:close(Socket); 201 | close_socket({ssl, Socket}) -> 202 | ssl:close(Socket). 203 | 204 | set_active_once(#state{socket = {tcp, Socket}}) -> 205 | ok = inet:setopts(Socket, [{active, once}]); 206 | set_active_once(#state{socket = {ssl, Socket}}) -> 207 | ok = ssl:setopts(Socket, [{active, once}]). 208 | 209 | handle_input(expecting_frame_header, 210 | <<"AMQP", Protocol/unsigned, Maj/unsigned, Min/unsigned, 211 | Rev/unsigned, Rest/binary>>, 212 | #state{connection = ConnectionPid} = State) 213 | when Protocol =:= 0 orelse Protocol =:= 3 -> 214 | ok = amqp10_client_connection:protocol_header_received( 215 | ConnectionPid, Protocol, Maj, Min, Rev), 216 | handle_input(expecting_frame_header, Rest, State); 217 | 218 | handle_input(expecting_frame_header, 219 | <>, State) 221 | when DOff >= 2 andalso (Type =:= 0 orelse Type =:= 1) -> 222 | AFS = #frame_state{frame_length = Length, channel = Channel, 223 | type = frame_type(Type), data_offset = DOff}, 224 | handle_input(expecting_extended_frame_header, Rest, 225 | State#state{frame_state = AFS}); 226 | 227 | handle_input(expecting_frame_header, <<_:8/binary, _/binary>>, State) -> 228 | {error, invalid_protocol_header, State}; 229 | 230 | handle_input(expecting_extended_frame_header, Data, 231 | #state{frame_state = 232 | #frame_state{data_offset = DOff}} = State) -> 233 | Skip = DOff * 4 - 8, 234 | case Data of 235 | <<_:Skip/binary, Rest/binary>> -> 236 | handle_input(expecting_frame_body, Rest, State); 237 | _ -> 238 | {ok, expecting_extended_frame_header, Data, State} 239 | end; 240 | 241 | handle_input(expecting_frame_body, Data, 242 | #state{frame_state = #frame_state{frame_length = Length, 243 | type = FrameType, 244 | data_offset = DOff, 245 | channel = Channel}} = State) -> 246 | Skip = DOff * 4 - 8, 247 | BodyLength = Length - Skip - 8, 248 | case {Data, BodyLength} of 249 | {<<_:BodyLength/binary, Rest/binary>>, 0} -> 250 | % heartbeat 251 | handle_input(expecting_frame_header, Rest, State); 252 | {<>, _} -> 253 | State1 = State#state{frame_state = undefined}, 254 | {PerfDesc, Payload} = amqp10_binary_parser:parse(FrameBody), 255 | Perf = amqp10_framing:decode(PerfDesc), 256 | State2 = route_frame(Channel, FrameType, {Perf, Payload}, State1), 257 | handle_input(expecting_frame_header, Rest, State2); 258 | _ -> 259 | {ok, expecting_frame_body, Data, State} 260 | end; 261 | 262 | handle_input(StateName, Data, State) -> 263 | {ok, StateName, Data, State}. 264 | 265 | %%% LOCAL 266 | 267 | defer_heartbeat_timer(State = 268 | #state{heartbeat_timer_ref = TRef, 269 | connection_config = #{idle_time_out := T}}) 270 | when is_number(T) andalso T > 0 -> 271 | _ = case TRef of 272 | undefined -> ok; 273 | _ -> _ = erlang:cancel_timer(TRef) 274 | end, 275 | NewTRef = erlang:send_after(T * 2, self(), heartbeat), 276 | State#state{heartbeat_timer_ref = NewTRef}; 277 | defer_heartbeat_timer(State) -> State. 278 | 279 | route_frame(Channel, FrameType, {Performative, Payload} = Frame, State0) -> 280 | {DestinationPid, State} = find_destination(Channel, FrameType, Performative, 281 | State0), 282 | ?DBG("FRAME -> ~p ~p~n ~p~n", [Channel, DestinationPid, Performative]), 283 | case Payload of 284 | <<>> -> ok = gen_statem:cast(DestinationPid, Performative); 285 | _ -> ok = gen_statem:cast(DestinationPid, Frame) 286 | end, 287 | State. 288 | 289 | -spec find_destination(amqp10_client_types:channel(), frame_type(), 290 | amqp10_client_types:amqp10_performative(), #state{}) -> 291 | {pid(), #state{}}. 292 | find_destination(0, amqp, Frame, #state{connection = ConnPid} = State) 293 | when is_record(Frame, 'v1_0.open') orelse 294 | is_record(Frame, 'v1_0.close') -> 295 | {ConnPid, State}; 296 | find_destination(_Channel, sasl, _Frame, 297 | #state{connection = ConnPid} = State) -> 298 | {ConnPid, State}; 299 | find_destination(Channel, amqp, 300 | #'v1_0.begin'{remote_channel = {ushort, OutgoingChannel}}, 301 | #state{outgoing_channels = OutgoingChannels, 302 | incoming_channels = IncomingChannels} = State) -> 303 | #{OutgoingChannel := Session} = OutgoingChannels, 304 | IncomingChannels1 = IncomingChannels#{Channel => Session}, 305 | State1 = State#state{incoming_channels = IncomingChannels1}, 306 | {Session, State1}; 307 | find_destination(Channel, amqp, _Frame, 308 | #state{incoming_channels = IncomingChannels} = State) -> 309 | #{Channel := Session} = IncomingChannels, 310 | {Session, State}. 311 | 312 | frame_type(0) -> amqp; 313 | frame_type(1) -> sasl. 314 | 315 | -ifdef(TEST). 316 | 317 | find_destination_test_() -> 318 | Pid = self(), 319 | State = #state{connection = Pid, outgoing_channels = #{3 => Pid}}, 320 | StateConn = #state{connection = Pid}, 321 | StateWithIncoming = State#state{incoming_channels = #{7 => Pid}}, 322 | StateWithIncoming0 = State#state{incoming_channels = #{0 => Pid}}, 323 | Tests = [{0, #'v1_0.open'{}, State, State, amqp}, 324 | {0, #'v1_0.close'{}, State, State, amqp}, 325 | {7, #'v1_0.begin'{remote_channel = {ushort, 3}}, State, 326 | StateWithIncoming, amqp}, 327 | {0, #'v1_0.begin'{remote_channel = {ushort, 3}}, State, 328 | StateWithIncoming0, amqp}, 329 | {7, #'v1_0.end'{}, StateWithIncoming, StateWithIncoming, amqp}, 330 | {7, #'v1_0.attach'{}, StateWithIncoming, StateWithIncoming, amqp}, 331 | {7, #'v1_0.flow'{}, StateWithIncoming, StateWithIncoming, amqp}, 332 | {0, #'v1_0.sasl_init'{}, StateConn, StateConn, sasl} 333 | ], 334 | [?_assertMatch({Pid, NewState}, 335 | find_destination(Channel, Type, Frame, InputState)) 336 | || {Channel, Frame, InputState, NewState, Type} <- Tests]. 337 | 338 | -endif. 339 | -------------------------------------------------------------------------------- /.github/workflows/test-erlang-otp-21.3.yaml: -------------------------------------------------------------------------------- 1 | # vim:sw=2:et: 2 | # https://help.github.com/en/actions/configuring-and-managing-workflows/configuring-a-workflow 3 | name: "Test - Erlang 21.3" 4 | on: 5 | push: 6 | repository_dispatch: 7 | types: 8 | - new-commit-to-dep-release-branch 9 | jobs: 10 | # vim:sw=2:et: 11 | checks: 12 | name: checks 13 | runs-on: ubuntu-18.04 14 | steps: 15 | - name: CHECKOUT REPOSITORY 16 | uses: actions/checkout@v2 17 | # https://github.com/marketplace/actions/setup-elixir 18 | - name: CONFIGURE OTP & ELIXIR 19 | uses: actions/setup-elixir@v1 20 | with: 21 | otp-version: 21.3 22 | # https://github.com/elixir-lang/elixir/releases 23 | elixir-version: 1.10.4 24 | - name: CHECK RABBITMQ COMPONENTS 25 | # https://github.community/t5/GitHub-Actions/How-can-I-set-an-expression-as-an-environment-variable-at/m-p/41804/highlight/true#M4751 26 | id: ref 27 | run: | 28 | branch_or_tag_name=${GITHUB_REF#refs/*/} 29 | echo "::set-output name=branch_or_tag_name::$branch_or_tag_name" 30 | make check-rabbitmq-components.mk base_rmq_ref=master current_rmq_ref=$branch_or_tag_name 31 | # https://help.github.com/en/actions/configuring-and-managing-workflows/caching-dependencies-to-speed-up-workflows 32 | - name: CACHE DEPS 33 | uses: actions/cache@v1 34 | with: 35 | path: deps 36 | key: otp-21.3_git-${{ github.sha }}_deps 37 | - name: RESOLVE & COMPILE DEPS 38 | run: | 39 | make deps test-deps base_rmq_ref=master current_rmq_ref=${{ steps.ref.outputs.branch_or_tag_name }} 40 | echo "Capture versions of the RabbitMQ components used in this workflow..." 41 | make amqp10_client-rabbitmq-deps.mk 42 | mv amqp10_client-rabbitmq-deps.mk deps/ 43 | echo "Remove directories not used in the subsequent jobs..." 44 | rm -fr deps/*/{.git,test} 45 | - name: UPLOAD DEPS VERSIONS 46 | uses: actions/upload-artifact@v2-preview 47 | with: 48 | name: amqp10_client-rabbitmq-deps.mk 49 | path: deps/amqp10_client-rabbitmq-deps.mk 50 | - name: CHECK CROSS REFERENCES 51 | run: | 52 | make xref base_rmq_ref=master current_rmq_ref=${{ steps.ref.outputs.branch_or_tag_name }} 53 | - name: COMPILE FOR TEST 54 | run: | 55 | make test-build base_rmq_ref=master current_rmq_ref=${{ steps.ref.outputs.branch_or_tag_name }} 56 | - name: CACHE SECONDARY UMBRELLAS 57 | if: success() && 'oldest' == 'oldest' 58 | uses: actions/cache@v1 59 | with: 60 | path: umbrellas 61 | key: secondary-umbrellas-v3.7.26-v3.8.3-erlang-21.3-rev3 62 | - name: PREPARE SECONDARY UMBRELLA COPIES 63 | if: success() && 'oldest' == 'oldest' 64 | run: | 65 | # ---------------------------------------------------------- 66 | # CAUTION: 67 | # The same script must be copied to `03-CT_SUITE.yaml`. It is used to 68 | # recreate the umbrellas if the cache restore fails. 69 | # ---------------------------------------------------------- 70 | set -x 71 | for version in v3.7.26 v3.8.3; do 72 | umbrella="umbrellas/$version" 73 | if ! test -d "$umbrella" || 74 | ! make -C "$umbrella/deps/amqp10_client" test-dist; then 75 | rm -rf "$umbrella" 76 | 77 | # Fetch the master Umbrella; the final umbrellas are created from 78 | # the master copy. 79 | if ! test -d umbrellas/master; then 80 | git config --global advice.detachedHead false 81 | git clone \ 82 | https://github.com/rabbitmq/rabbitmq-public-umbrella.git \ 83 | umbrellas/master 84 | make -C umbrellas/master co # To get RabbitMQ components. 85 | fi 86 | 87 | # We copy the master Umbrella and checkout the appropriate tag. 88 | cp -a umbrellas/master "$umbrella" 89 | git -C "$umbrella" checkout "master" 90 | make -C "$umbrella" up BRANCH="$version" 91 | # To remove third-party deps which were checked out when the 92 | # projects were on the `master` branch. Thus, possibly not the 93 | # version pinning we expect. We update the Umbrella one last time 94 | # to fetch the correct third-party deps. 95 | make -C "$umbrella" clean-3rd-party-repos 96 | make -C "$umbrella" up 97 | make -C "$umbrella/deps/amqp10_client" test-dist 98 | rm -fr "$umbrella"/deps/*/{.git,test} "$umbrella"/.git 99 | fi 100 | done 101 | rm -fr umbrellas/master 102 | # vim:sw=2:et: 103 | dialyzer: 104 | name: dialyzer 105 | needs: [checks] 106 | runs-on: ubuntu-18.04 107 | steps: 108 | - name: CHECKOUT REPOSITORY 109 | if: success() && 'oldest' == 'latest' 110 | uses: actions/checkout@v2 111 | # https://github.com/marketplace/actions/setup-elixir 112 | - name: CONFIGURE OTP & ELIXIR 113 | if: success() && 'oldest' == 'latest' 114 | uses: actions/setup-elixir@v1 115 | with: 116 | otp-version: 21.3 117 | # https://github.com/elixir-lang/elixir/releases 118 | elixir-version: 1.10.4 119 | - name: CACHE DEPS 120 | if: success() && 'oldest' == 'latest' 121 | uses: actions/cache@v1 122 | with: 123 | path: deps 124 | key: otp-21.3_git-${{ github.sha }}_deps 125 | - name: CACHE DIALYZER PLT 126 | if: success() && 'oldest' == 'latest' 127 | uses: actions/cache@v1 128 | with: 129 | path: .amqp10_client.plt 130 | key: plt-amqp10_client-erlang-21.3-g${{ github.sha }} 131 | - name: RUN DIALYZER 132 | if: success() && 'oldest' == 'latest' 133 | run: | 134 | branch_or_tag_name=${GITHUB_REF#refs/*/} 135 | make dialyze \ 136 | base_rmq_ref=master \ 137 | current_rmq_ref=$branch_or_tag_name \ 138 | FULL= 139 | # vim:sw=2:et: 140 | eunit: 141 | needs: [checks] 142 | # https://help.github.com/en/actions/reference/context-and-expression-syntax-for-github-actions#contexts 143 | name: eunit 144 | runs-on: ubuntu-18.04 145 | steps: 146 | - name: CHECKOUT REPOSITORY 147 | uses: actions/checkout@v2 148 | # https://github.com/marketplace/actions/setup-elixir 149 | - name: CONFIGURE OTP & ELIXIR 150 | uses: actions/setup-elixir@v1 151 | with: 152 | otp-version: 21.3 153 | # https://github.com/elixir-lang/elixir/releases 154 | elixir-version: 1.10.4 155 | - name: CACHE DEPS 156 | uses: actions/cache@v1 157 | with: 158 | path: deps 159 | key: otp-21.3_git-${{ github.sha }}_deps 160 | - name: RUN TESTS 161 | run: | 162 | ! test -d ebin || touch ebin/* 163 | branch_or_tag_name=${GITHUB_REF#refs/*/} 164 | make eunit \ 165 | base_rmq_ref=master \ 166 | current_rmq_ref=$branch_or_tag_name \ 167 | FULL= \ 168 | FAIL_FAST=1 \ 169 | SKIP_AS_ERROR=1 170 | # vim:sw=2:et: 171 | ct-msg: 172 | needs: [checks] 173 | # https://help.github.com/en/actions/reference/context-and-expression-syntax-for-github-actions#contexts 174 | name: ct-msg 175 | runs-on: ubuntu-18.04 176 | steps: 177 | - name: CHECKOUT REPOSITORY 178 | uses: actions/checkout@v2 179 | # https://github.com/marketplace/actions/setup-elixir 180 | - name: CONFIGURE OTP & ELIXIR 181 | uses: actions/setup-elixir@v1 182 | with: 183 | otp-version: 21.3 184 | # https://github.com/elixir-lang/elixir/releases 185 | elixir-version: 1.10.4 186 | - name: CACHE DEPS 187 | uses: actions/cache@v1 188 | with: 189 | path: deps 190 | key: otp-21.3_git-${{ github.sha }}_deps 191 | - name: RUN TESTS 192 | run: | 193 | branch_or_tag_name=${GITHUB_REF#refs/*/} 194 | ! test -d ebin || touch ebin/* 195 | make ct-msg \ 196 | base_rmq_ref=master \ 197 | current_rmq_ref=$branch_or_tag_name \ 198 | FULL= \ 199 | FAIL_FAST=1 \ 200 | SKIP_AS_ERROR=1 201 | - name: CACHE SECONDARY UMBRELLAS 202 | if: success() && 'oldest' == 'oldest' 203 | uses: actions/cache@v1 204 | with: 205 | path: umbrellas 206 | key: secondary-umbrellas-v3.7.26-v3.8.3-erlang-21.3-rev3 207 | - name: VERIFY SECONDARY UMBRELLA COPIES (in case of cache restore failure) 208 | if: success() && 'oldest' == 'oldest' 209 | run: | 210 | set -x 211 | for version in v3.7.26 v3.8.3; do 212 | umbrella="umbrellas/$version" 213 | if ! test -d "$umbrella" || 214 | ! make -C "$umbrella/deps/amqp10_client" test-dist; then 215 | rm -rf "$umbrella" 216 | 217 | # Fetch the master Umbrella; the final umbrellas are created from 218 | # the master copy. 219 | if ! test -d umbrellas/master; then 220 | git config --global advice.detachedHead false 221 | git clone \ 222 | https://github.com/rabbitmq/rabbitmq-public-umbrella.git \ 223 | umbrellas/master 224 | make -C umbrellas/master co # To get RabbitMQ components. 225 | fi 226 | 227 | # We copy the master Umbrella and checkout the appropriate tag. 228 | cp -a umbrellas/master "$umbrella" 229 | git -C "$umbrella" checkout "master" 230 | make -C "$umbrella" up BRANCH="$version" 231 | # To remove third-party deps which were checked out when the 232 | # projects were on the `master` branch. Thus, possibly not the 233 | # version pinning we expect. We update the Umbrella one last time 234 | # to fetch the correct third-party deps. 235 | make -C "$umbrella" clean-3rd-party-repos 236 | make -C "$umbrella" up 237 | make -C "$umbrella/deps/amqp10_client" test-dist 238 | rm -fr "$umbrella"/deps/*/{.git,test} "$umbrella"/.git 239 | fi 240 | done 241 | rm -fr umbrellas/master 242 | - name: RUN TESTS [mixed-versions] 243 | if: success() && 'oldest' == 'oldest' 244 | run: | 245 | set -x 246 | branch_or_tag_name=${GITHUB_REF#refs/*/} 247 | for umbrella in umbrellas/*; do 248 | test -d "$umbrella" 249 | printf '\n\033[1;32mMixing clusters with RabbitMQ %s\033[0m' \ 250 | $(basename "$umbrella") 251 | make distclean-ct ct-msg \ 252 | base_rmq_ref=master \ 253 | current_rmq_ref=$branch_or_tag_name \ 254 | FULL= \ 255 | FAIL_FAST=1 \ 256 | SKIP_AS_ERROR=1 \ 257 | SECONDARY_UMBRELLA=$PWD/$umbrella \ 258 | RABBITMQ_FEATURE_FLAGS= 259 | done 260 | - name: ON FAILURE ARCHIVE TESTS LOGS 261 | if: failure() 262 | run: | 263 | make ct-logs-archive 264 | - name: ON FAILURE UPLOAD TESTS LOGS ARTIFACT 265 | # https://github.com/marketplace/actions/upload-artifact 266 | uses: actions/upload-artifact@v2-preview 267 | if: failure() 268 | with: 269 | name: ct-msg-logs 270 | path: "*-ct-logs-*.tar.xz" 271 | # vim:sw=2:et: 272 | ct-system: 273 | needs: [checks] 274 | # https://help.github.com/en/actions/reference/context-and-expression-syntax-for-github-actions#contexts 275 | name: ct-system 276 | runs-on: ubuntu-18.04 277 | steps: 278 | - name: CHECKOUT REPOSITORY 279 | uses: actions/checkout@v2 280 | # https://github.com/marketplace/actions/setup-elixir 281 | - name: CONFIGURE OTP & ELIXIR 282 | uses: actions/setup-elixir@v1 283 | with: 284 | otp-version: 21.3 285 | # https://github.com/elixir-lang/elixir/releases 286 | elixir-version: 1.10.4 287 | - name: CACHE DEPS 288 | uses: actions/cache@v1 289 | with: 290 | path: deps 291 | key: otp-21.3_git-${{ github.sha }}_deps 292 | - name: RUN TESTS 293 | run: | 294 | branch_or_tag_name=${GITHUB_REF#refs/*/} 295 | ! test -d ebin || touch ebin/* 296 | make ct-system \ 297 | base_rmq_ref=master \ 298 | current_rmq_ref=$branch_or_tag_name \ 299 | FULL= \ 300 | FAIL_FAST=1 \ 301 | SKIP_AS_ERROR=1 302 | - name: CACHE SECONDARY UMBRELLAS 303 | if: success() && 'oldest' == 'oldest' 304 | uses: actions/cache@v1 305 | with: 306 | path: umbrellas 307 | key: secondary-umbrellas-v3.7.26-v3.8.3-erlang-21.3-rev3 308 | - name: VERIFY SECONDARY UMBRELLA COPIES (in case of cache restore failure) 309 | if: success() && 'oldest' == 'oldest' 310 | run: | 311 | set -x 312 | for version in v3.7.26 v3.8.3; do 313 | umbrella="umbrellas/$version" 314 | if ! test -d "$umbrella" || 315 | ! make -C "$umbrella/deps/amqp10_client" test-dist; then 316 | rm -rf "$umbrella" 317 | 318 | # Fetch the master Umbrella; the final umbrellas are created from 319 | # the master copy. 320 | if ! test -d umbrellas/master; then 321 | git config --global advice.detachedHead false 322 | git clone \ 323 | https://github.com/rabbitmq/rabbitmq-public-umbrella.git \ 324 | umbrellas/master 325 | make -C umbrellas/master co # To get RabbitMQ components. 326 | fi 327 | 328 | # We copy the master Umbrella and checkout the appropriate tag. 329 | cp -a umbrellas/master "$umbrella" 330 | git -C "$umbrella" checkout "master" 331 | make -C "$umbrella" up BRANCH="$version" 332 | # To remove third-party deps which were checked out when the 333 | # projects were on the `master` branch. Thus, possibly not the 334 | # version pinning we expect. We update the Umbrella one last time 335 | # to fetch the correct third-party deps. 336 | make -C "$umbrella" clean-3rd-party-repos 337 | make -C "$umbrella" up 338 | make -C "$umbrella/deps/amqp10_client" test-dist 339 | rm -fr "$umbrella"/deps/*/{.git,test} "$umbrella"/.git 340 | fi 341 | done 342 | rm -fr umbrellas/master 343 | - name: RUN TESTS [mixed-versions] 344 | if: success() && 'oldest' == 'oldest' 345 | run: | 346 | set -x 347 | branch_or_tag_name=${GITHUB_REF#refs/*/} 348 | for umbrella in umbrellas/*; do 349 | test -d "$umbrella" 350 | printf '\n\033[1;32mMixing clusters with RabbitMQ %s\033[0m' \ 351 | $(basename "$umbrella") 352 | make distclean-ct ct-system \ 353 | base_rmq_ref=master \ 354 | current_rmq_ref=$branch_or_tag_name \ 355 | FULL= \ 356 | FAIL_FAST=1 \ 357 | SKIP_AS_ERROR=1 \ 358 | SECONDARY_UMBRELLA=$PWD/$umbrella \ 359 | RABBITMQ_FEATURE_FLAGS= 360 | done 361 | - name: ON FAILURE ARCHIVE TESTS LOGS 362 | if: failure() 363 | run: | 364 | make ct-logs-archive 365 | - name: ON FAILURE UPLOAD TESTS LOGS ARTIFACT 366 | # https://github.com/marketplace/actions/upload-artifact 367 | uses: actions/upload-artifact@v2-preview 368 | if: failure() 369 | with: 370 | name: ct-system-logs 371 | path: "*-ct-logs-*.tar.xz" 372 | # vim:sw=2:et: 373 | capture-tested-deps-versions: 374 | needs: 375 | - dialyzer 376 | - eunit 377 | - ct-msg 378 | - ct-system 379 | runs-on: ubuntu-18.04 380 | steps: 381 | - name: CHECKOUT REPOSITORY 382 | uses: actions/checkout@v2 383 | - name: CACHE DEPS 384 | uses: actions/cache@v1 385 | with: 386 | path: deps 387 | key: otp-21.3_git-${{ github.sha }}_deps 388 | - name: FORMAT GIT REF 389 | # https://github.community/t5/GitHub-Actions/How-can-I-set-an-expression-as-an-environment-variable-at/m-p/41804/highlight/true#M4751 390 | id: ref 391 | run: | 392 | branch_or_tag_name=${GITHUB_REF#refs/*/} 393 | echo "::set-output name=branch_or_tag_name::$branch_or_tag_name" 394 | - name: UPLOAD TO S3 395 | if: github.ref == 'refs/heads/master' 396 | # https://github.com/marketplace/actions/s3-file-upload 397 | uses: zdurham/s3-upload-github-action@master 398 | env: 399 | AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} 400 | AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} 401 | AWS_REGION: ${{ secrets.AWS_REGION }} 402 | FILE: deps/amqp10_client-rabbitmq-deps.mk 403 | S3_BUCKET: ${{ secrets.AWS_S3_BUCKET }} 404 | S3_KEY: rabbitmq-amqp1.0-client/${{ steps.ref.outputs.branch_or_tag_name }}/${{ github.run_id }}/otp-21.3/amqp10_client-rabbitmq-deps.mk 405 | -------------------------------------------------------------------------------- /.github/workflows/test-erlang-otp-22.3.yaml: -------------------------------------------------------------------------------- 1 | # vim:sw=2:et: 2 | # https://help.github.com/en/actions/configuring-and-managing-workflows/configuring-a-workflow 3 | name: "Test - Erlang 22.3" 4 | on: 5 | push: 6 | repository_dispatch: 7 | types: 8 | - new-commit-to-dep-release-branch 9 | jobs: 10 | # vim:sw=2:et: 11 | checks: 12 | name: checks 13 | runs-on: ubuntu-18.04 14 | steps: 15 | - name: CHECKOUT REPOSITORY 16 | uses: actions/checkout@v2 17 | # https://github.com/marketplace/actions/setup-elixir 18 | - name: CONFIGURE OTP & ELIXIR 19 | uses: actions/setup-elixir@v1 20 | with: 21 | otp-version: 22.3 22 | # https://github.com/elixir-lang/elixir/releases 23 | elixir-version: 1.10.4 24 | - name: CHECK RABBITMQ COMPONENTS 25 | # https://github.community/t5/GitHub-Actions/How-can-I-set-an-expression-as-an-environment-variable-at/m-p/41804/highlight/true#M4751 26 | id: ref 27 | run: | 28 | branch_or_tag_name=${GITHUB_REF#refs/*/} 29 | echo "::set-output name=branch_or_tag_name::$branch_or_tag_name" 30 | make check-rabbitmq-components.mk base_rmq_ref=master current_rmq_ref=$branch_or_tag_name 31 | # https://help.github.com/en/actions/configuring-and-managing-workflows/caching-dependencies-to-speed-up-workflows 32 | - name: CACHE DEPS 33 | uses: actions/cache@v1 34 | with: 35 | path: deps 36 | key: otp-22.3_git-${{ github.sha }}_deps 37 | - name: RESOLVE & COMPILE DEPS 38 | run: | 39 | make deps test-deps base_rmq_ref=master current_rmq_ref=${{ steps.ref.outputs.branch_or_tag_name }} 40 | echo "Capture versions of the RabbitMQ components used in this workflow..." 41 | make amqp10_client-rabbitmq-deps.mk 42 | mv amqp10_client-rabbitmq-deps.mk deps/ 43 | echo "Remove directories not used in the subsequent jobs..." 44 | rm -fr deps/*/{.git,test} 45 | - name: UPLOAD DEPS VERSIONS 46 | uses: actions/upload-artifact@v2-preview 47 | with: 48 | name: amqp10_client-rabbitmq-deps.mk 49 | path: deps/amqp10_client-rabbitmq-deps.mk 50 | - name: CHECK CROSS REFERENCES 51 | run: | 52 | make xref base_rmq_ref=master current_rmq_ref=${{ steps.ref.outputs.branch_or_tag_name }} 53 | - name: COMPILE FOR TEST 54 | run: | 55 | make test-build base_rmq_ref=master current_rmq_ref=${{ steps.ref.outputs.branch_or_tag_name }} 56 | - name: CACHE SECONDARY UMBRELLAS 57 | if: success() && 'latest' == 'oldest' 58 | uses: actions/cache@v1 59 | with: 60 | path: umbrellas 61 | key: secondary-umbrellas-v3.7.26-v3.8.3-erlang-22.3-rev3 62 | - name: PREPARE SECONDARY UMBRELLA COPIES 63 | if: success() && 'latest' == 'oldest' 64 | run: | 65 | # ---------------------------------------------------------- 66 | # CAUTION: 67 | # The same script must be copied to `03-CT_SUITE.yaml`. It is used to 68 | # recreate the umbrellas if the cache restore fails. 69 | # ---------------------------------------------------------- 70 | set -x 71 | for version in v3.7.26 v3.8.3; do 72 | umbrella="umbrellas/$version" 73 | if ! test -d "$umbrella" || 74 | ! make -C "$umbrella/deps/amqp10_client" test-dist; then 75 | rm -rf "$umbrella" 76 | 77 | # Fetch the master Umbrella; the final umbrellas are created from 78 | # the master copy. 79 | if ! test -d umbrellas/master; then 80 | git config --global advice.detachedHead false 81 | git clone \ 82 | https://github.com/rabbitmq/rabbitmq-public-umbrella.git \ 83 | umbrellas/master 84 | make -C umbrellas/master co # To get RabbitMQ components. 85 | fi 86 | 87 | # We copy the master Umbrella and checkout the appropriate tag. 88 | cp -a umbrellas/master "$umbrella" 89 | git -C "$umbrella" checkout "master" 90 | make -C "$umbrella" up BRANCH="$version" 91 | # To remove third-party deps which were checked out when the 92 | # projects were on the `master` branch. Thus, possibly not the 93 | # version pinning we expect. We update the Umbrella one last time 94 | # to fetch the correct third-party deps. 95 | make -C "$umbrella" clean-3rd-party-repos 96 | make -C "$umbrella" up 97 | make -C "$umbrella/deps/amqp10_client" test-dist 98 | rm -fr "$umbrella"/deps/*/{.git,test} "$umbrella"/.git 99 | fi 100 | done 101 | rm -fr umbrellas/master 102 | # vim:sw=2:et: 103 | dialyzer: 104 | name: dialyzer 105 | needs: [checks] 106 | runs-on: ubuntu-18.04 107 | steps: 108 | - name: CHECKOUT REPOSITORY 109 | if: success() && 'latest' == 'latest' 110 | uses: actions/checkout@v2 111 | # https://github.com/marketplace/actions/setup-elixir 112 | - name: CONFIGURE OTP & ELIXIR 113 | if: success() && 'latest' == 'latest' 114 | uses: actions/setup-elixir@v1 115 | with: 116 | otp-version: 22.3 117 | # https://github.com/elixir-lang/elixir/releases 118 | elixir-version: 1.10.4 119 | - name: CACHE DEPS 120 | if: success() && 'latest' == 'latest' 121 | uses: actions/cache@v1 122 | with: 123 | path: deps 124 | key: otp-22.3_git-${{ github.sha }}_deps 125 | - name: CACHE DIALYZER PLT 126 | if: success() && 'latest' == 'latest' 127 | uses: actions/cache@v1 128 | with: 129 | path: .amqp10_client.plt 130 | key: plt-amqp10_client-erlang-22.3-g${{ github.sha }} 131 | - name: RUN DIALYZER 132 | if: success() && 'latest' == 'latest' 133 | run: | 134 | branch_or_tag_name=${GITHUB_REF#refs/*/} 135 | make dialyze \ 136 | base_rmq_ref=master \ 137 | current_rmq_ref=$branch_or_tag_name \ 138 | FULL= 139 | # vim:sw=2:et: 140 | eunit: 141 | needs: [checks] 142 | # https://help.github.com/en/actions/reference/context-and-expression-syntax-for-github-actions#contexts 143 | name: eunit 144 | runs-on: ubuntu-18.04 145 | steps: 146 | - name: CHECKOUT REPOSITORY 147 | uses: actions/checkout@v2 148 | # https://github.com/marketplace/actions/setup-elixir 149 | - name: CONFIGURE OTP & ELIXIR 150 | uses: actions/setup-elixir@v1 151 | with: 152 | otp-version: 22.3 153 | # https://github.com/elixir-lang/elixir/releases 154 | elixir-version: 1.10.4 155 | - name: CACHE DEPS 156 | uses: actions/cache@v1 157 | with: 158 | path: deps 159 | key: otp-22.3_git-${{ github.sha }}_deps 160 | - name: RUN TESTS 161 | run: | 162 | ! test -d ebin || touch ebin/* 163 | branch_or_tag_name=${GITHUB_REF#refs/*/} 164 | make eunit \ 165 | base_rmq_ref=master \ 166 | current_rmq_ref=$branch_or_tag_name \ 167 | FULL= \ 168 | FAIL_FAST=1 \ 169 | SKIP_AS_ERROR=1 170 | # vim:sw=2:et: 171 | ct-msg: 172 | needs: [checks] 173 | # https://help.github.com/en/actions/reference/context-and-expression-syntax-for-github-actions#contexts 174 | name: ct-msg 175 | runs-on: ubuntu-18.04 176 | steps: 177 | - name: CHECKOUT REPOSITORY 178 | uses: actions/checkout@v2 179 | # https://github.com/marketplace/actions/setup-elixir 180 | - name: CONFIGURE OTP & ELIXIR 181 | uses: actions/setup-elixir@v1 182 | with: 183 | otp-version: 22.3 184 | # https://github.com/elixir-lang/elixir/releases 185 | elixir-version: 1.10.4 186 | - name: CACHE DEPS 187 | uses: actions/cache@v1 188 | with: 189 | path: deps 190 | key: otp-22.3_git-${{ github.sha }}_deps 191 | - name: RUN TESTS 192 | run: | 193 | branch_or_tag_name=${GITHUB_REF#refs/*/} 194 | ! test -d ebin || touch ebin/* 195 | make ct-msg \ 196 | base_rmq_ref=master \ 197 | current_rmq_ref=$branch_or_tag_name \ 198 | FULL= \ 199 | FAIL_FAST=1 \ 200 | SKIP_AS_ERROR=1 201 | - name: CACHE SECONDARY UMBRELLAS 202 | if: success() && 'latest' == 'oldest' 203 | uses: actions/cache@v1 204 | with: 205 | path: umbrellas 206 | key: secondary-umbrellas-v3.7.26-v3.8.3-erlang-22.3-rev3 207 | - name: VERIFY SECONDARY UMBRELLA COPIES (in case of cache restore failure) 208 | if: success() && 'latest' == 'oldest' 209 | run: | 210 | set -x 211 | for version in v3.7.26 v3.8.3; do 212 | umbrella="umbrellas/$version" 213 | if ! test -d "$umbrella" || 214 | ! make -C "$umbrella/deps/amqp10_client" test-dist; then 215 | rm -rf "$umbrella" 216 | 217 | # Fetch the master Umbrella; the final umbrellas are created from 218 | # the master copy. 219 | if ! test -d umbrellas/master; then 220 | git config --global advice.detachedHead false 221 | git clone \ 222 | https://github.com/rabbitmq/rabbitmq-public-umbrella.git \ 223 | umbrellas/master 224 | make -C umbrellas/master co # To get RabbitMQ components. 225 | fi 226 | 227 | # We copy the master Umbrella and checkout the appropriate tag. 228 | cp -a umbrellas/master "$umbrella" 229 | git -C "$umbrella" checkout "master" 230 | make -C "$umbrella" up BRANCH="$version" 231 | # To remove third-party deps which were checked out when the 232 | # projects were on the `master` branch. Thus, possibly not the 233 | # version pinning we expect. We update the Umbrella one last time 234 | # to fetch the correct third-party deps. 235 | make -C "$umbrella" clean-3rd-party-repos 236 | make -C "$umbrella" up 237 | make -C "$umbrella/deps/amqp10_client" test-dist 238 | rm -fr "$umbrella"/deps/*/{.git,test} "$umbrella"/.git 239 | fi 240 | done 241 | rm -fr umbrellas/master 242 | - name: RUN TESTS [mixed-versions] 243 | if: success() && 'latest' == 'oldest' 244 | run: | 245 | set -x 246 | branch_or_tag_name=${GITHUB_REF#refs/*/} 247 | for umbrella in umbrellas/*; do 248 | test -d "$umbrella" 249 | printf '\n\033[1;32mMixing clusters with RabbitMQ %s\033[0m' \ 250 | $(basename "$umbrella") 251 | make distclean-ct ct-msg \ 252 | base_rmq_ref=master \ 253 | current_rmq_ref=$branch_or_tag_name \ 254 | FULL= \ 255 | FAIL_FAST=1 \ 256 | SKIP_AS_ERROR=1 \ 257 | SECONDARY_UMBRELLA=$PWD/$umbrella \ 258 | RABBITMQ_FEATURE_FLAGS= 259 | done 260 | - name: ON FAILURE ARCHIVE TESTS LOGS 261 | if: failure() 262 | run: | 263 | make ct-logs-archive 264 | - name: ON FAILURE UPLOAD TESTS LOGS ARTIFACT 265 | # https://github.com/marketplace/actions/upload-artifact 266 | uses: actions/upload-artifact@v2-preview 267 | if: failure() 268 | with: 269 | name: ct-msg-logs 270 | path: "*-ct-logs-*.tar.xz" 271 | # vim:sw=2:et: 272 | ct-system: 273 | needs: [checks] 274 | # https://help.github.com/en/actions/reference/context-and-expression-syntax-for-github-actions#contexts 275 | name: ct-system 276 | runs-on: ubuntu-18.04 277 | steps: 278 | - name: CHECKOUT REPOSITORY 279 | uses: actions/checkout@v2 280 | # https://github.com/marketplace/actions/setup-elixir 281 | - name: CONFIGURE OTP & ELIXIR 282 | uses: actions/setup-elixir@v1 283 | with: 284 | otp-version: 22.3 285 | # https://github.com/elixir-lang/elixir/releases 286 | elixir-version: 1.10.4 287 | - name: CACHE DEPS 288 | uses: actions/cache@v1 289 | with: 290 | path: deps 291 | key: otp-22.3_git-${{ github.sha }}_deps 292 | - name: RUN TESTS 293 | run: | 294 | branch_or_tag_name=${GITHUB_REF#refs/*/} 295 | ! test -d ebin || touch ebin/* 296 | make ct-system \ 297 | base_rmq_ref=master \ 298 | current_rmq_ref=$branch_or_tag_name \ 299 | FULL= \ 300 | FAIL_FAST=1 \ 301 | SKIP_AS_ERROR=1 302 | - name: CACHE SECONDARY UMBRELLAS 303 | if: success() && 'latest' == 'oldest' 304 | uses: actions/cache@v1 305 | with: 306 | path: umbrellas 307 | key: secondary-umbrellas-v3.7.26-v3.8.3-erlang-22.3-rev3 308 | - name: VERIFY SECONDARY UMBRELLA COPIES (in case of cache restore failure) 309 | if: success() && 'latest' == 'oldest' 310 | run: | 311 | set -x 312 | for version in v3.7.26 v3.8.3; do 313 | umbrella="umbrellas/$version" 314 | if ! test -d "$umbrella" || 315 | ! make -C "$umbrella/deps/amqp10_client" test-dist; then 316 | rm -rf "$umbrella" 317 | 318 | # Fetch the master Umbrella; the final umbrellas are created from 319 | # the master copy. 320 | if ! test -d umbrellas/master; then 321 | git config --global advice.detachedHead false 322 | git clone \ 323 | https://github.com/rabbitmq/rabbitmq-public-umbrella.git \ 324 | umbrellas/master 325 | make -C umbrellas/master co # To get RabbitMQ components. 326 | fi 327 | 328 | # We copy the master Umbrella and checkout the appropriate tag. 329 | cp -a umbrellas/master "$umbrella" 330 | git -C "$umbrella" checkout "master" 331 | make -C "$umbrella" up BRANCH="$version" 332 | # To remove third-party deps which were checked out when the 333 | # projects were on the `master` branch. Thus, possibly not the 334 | # version pinning we expect. We update the Umbrella one last time 335 | # to fetch the correct third-party deps. 336 | make -C "$umbrella" clean-3rd-party-repos 337 | make -C "$umbrella" up 338 | make -C "$umbrella/deps/amqp10_client" test-dist 339 | rm -fr "$umbrella"/deps/*/{.git,test} "$umbrella"/.git 340 | fi 341 | done 342 | rm -fr umbrellas/master 343 | - name: RUN TESTS [mixed-versions] 344 | if: success() && 'latest' == 'oldest' 345 | run: | 346 | set -x 347 | branch_or_tag_name=${GITHUB_REF#refs/*/} 348 | for umbrella in umbrellas/*; do 349 | test -d "$umbrella" 350 | printf '\n\033[1;32mMixing clusters with RabbitMQ %s\033[0m' \ 351 | $(basename "$umbrella") 352 | make distclean-ct ct-system \ 353 | base_rmq_ref=master \ 354 | current_rmq_ref=$branch_or_tag_name \ 355 | FULL= \ 356 | FAIL_FAST=1 \ 357 | SKIP_AS_ERROR=1 \ 358 | SECONDARY_UMBRELLA=$PWD/$umbrella \ 359 | RABBITMQ_FEATURE_FLAGS= 360 | done 361 | - name: ON FAILURE ARCHIVE TESTS LOGS 362 | if: failure() 363 | run: | 364 | make ct-logs-archive 365 | - name: ON FAILURE UPLOAD TESTS LOGS ARTIFACT 366 | # https://github.com/marketplace/actions/upload-artifact 367 | uses: actions/upload-artifact@v2-preview 368 | if: failure() 369 | with: 370 | name: ct-system-logs 371 | path: "*-ct-logs-*.tar.xz" 372 | # vim:sw=2:et: 373 | capture-tested-deps-versions: 374 | needs: 375 | - dialyzer 376 | - eunit 377 | - ct-msg 378 | - ct-system 379 | runs-on: ubuntu-18.04 380 | steps: 381 | - name: CHECKOUT REPOSITORY 382 | uses: actions/checkout@v2 383 | - name: CACHE DEPS 384 | uses: actions/cache@v1 385 | with: 386 | path: deps 387 | key: otp-22.3_git-${{ github.sha }}_deps 388 | - name: FORMAT GIT REF 389 | # https://github.community/t5/GitHub-Actions/How-can-I-set-an-expression-as-an-environment-variable-at/m-p/41804/highlight/true#M4751 390 | id: ref 391 | run: | 392 | branch_or_tag_name=${GITHUB_REF#refs/*/} 393 | echo "::set-output name=branch_or_tag_name::$branch_or_tag_name" 394 | - name: UPLOAD TO S3 395 | if: github.ref == 'refs/heads/master' 396 | # https://github.com/marketplace/actions/s3-file-upload 397 | uses: zdurham/s3-upload-github-action@master 398 | env: 399 | AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} 400 | AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} 401 | AWS_REGION: ${{ secrets.AWS_REGION }} 402 | FILE: deps/amqp10_client-rabbitmq-deps.mk 403 | S3_BUCKET: ${{ secrets.AWS_S3_BUCKET }} 404 | S3_KEY: rabbitmq-amqp1.0-client/${{ steps.ref.outputs.branch_or_tag_name }}/${{ github.run_id }}/otp-22.3/amqp10_client-rabbitmq-deps.mk 405 | -------------------------------------------------------------------------------- /LICENSE-MPL-RabbitMQ: -------------------------------------------------------------------------------- 1 | Mozilla Public License Version 2.0 2 | ================================== 3 | 4 | 1. Definitions 5 | -------------- 6 | 7 | 1.1. "Contributor" 8 | means each individual or legal entity that creates, contributes to 9 | the creation of, or owns Covered Software. 10 | 11 | 1.2. "Contributor Version" 12 | means the combination of the Contributions of others (if any) used 13 | by a Contributor and that particular Contributor's Contribution. 14 | 15 | 1.3. "Contribution" 16 | means Covered Software of a particular Contributor. 17 | 18 | 1.4. "Covered Software" 19 | means Source Code Form to which the initial Contributor has attached 20 | the notice in Exhibit A, the Executable Form of such Source Code 21 | Form, and Modifications of such Source Code Form, in each case 22 | including portions thereof. 23 | 24 | 1.5. "Incompatible With Secondary Licenses" 25 | means 26 | 27 | (a) that the initial Contributor has attached the notice described 28 | in Exhibit B to the Covered Software; or 29 | 30 | (b) that the Covered Software was made available under the terms of 31 | version 1.1 or earlier of the License, but not also under the 32 | terms of a Secondary License. 33 | 34 | 1.6. "Executable Form" 35 | means any form of the work other than Source Code Form. 36 | 37 | 1.7. "Larger Work" 38 | means a work that combines Covered Software with other material, in 39 | a separate file or files, that is not Covered Software. 40 | 41 | 1.8. "License" 42 | means this document. 43 | 44 | 1.9. "Licensable" 45 | means having the right to grant, to the maximum extent possible, 46 | whether at the time of the initial grant or subsequently, any and 47 | all of the rights conveyed by this License. 48 | 49 | 1.10. "Modifications" 50 | means any of the following: 51 | 52 | (a) any file in Source Code Form that results from an addition to, 53 | deletion from, or modification of the contents of Covered 54 | Software; or 55 | 56 | (b) any new file in Source Code Form that contains any Covered 57 | Software. 58 | 59 | 1.11. "Patent Claims" of a Contributor 60 | means any patent claim(s), including without limitation, method, 61 | process, and apparatus claims, in any patent Licensable by such 62 | Contributor that would be infringed, but for the grant of the 63 | License, by the making, using, selling, offering for sale, having 64 | made, import, or transfer of either its Contributions or its 65 | Contributor Version. 66 | 67 | 1.12. "Secondary License" 68 | means either the GNU General Public License, Version 2.0, the GNU 69 | Lesser General Public License, Version 2.1, the GNU Affero General 70 | Public License, Version 3.0, or any later versions of those 71 | licenses. 72 | 73 | 1.13. "Source Code Form" 74 | means the form of the work preferred for making modifications. 75 | 76 | 1.14. "You" (or "Your") 77 | means an individual or a legal entity exercising rights under this 78 | License. For legal entities, "You" includes any entity that 79 | controls, is controlled by, or is under common control with You. For 80 | purposes of this definition, "control" means (a) the power, direct 81 | or indirect, to cause the direction or management of such entity, 82 | whether by contract or otherwise, or (b) ownership of more than 83 | fifty percent (50%) of the outstanding shares or beneficial 84 | ownership of such entity. 85 | 86 | 2. License Grants and Conditions 87 | -------------------------------- 88 | 89 | 2.1. Grants 90 | 91 | Each Contributor hereby grants You a world-wide, royalty-free, 92 | non-exclusive license: 93 | 94 | (a) under intellectual property rights (other than patent or trademark) 95 | Licensable by such Contributor to use, reproduce, make available, 96 | modify, display, perform, distribute, and otherwise exploit its 97 | Contributions, either on an unmodified basis, with Modifications, or 98 | as part of a Larger Work; and 99 | 100 | (b) under Patent Claims of such Contributor to make, use, sell, offer 101 | for sale, have made, import, and otherwise transfer either its 102 | Contributions or its Contributor Version. 103 | 104 | 2.2. Effective Date 105 | 106 | The licenses granted in Section 2.1 with respect to any Contribution 107 | become effective for each Contribution on the date the Contributor first 108 | distributes such Contribution. 109 | 110 | 2.3. Limitations on Grant Scope 111 | 112 | The licenses granted in this Section 2 are the only rights granted under 113 | this License. No additional rights or licenses will be implied from the 114 | distribution or licensing of Covered Software under this License. 115 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 116 | Contributor: 117 | 118 | (a) for any code that a Contributor has removed from Covered Software; 119 | or 120 | 121 | (b) for infringements caused by: (i) Your and any other third party's 122 | modifications of Covered Software, or (ii) the combination of its 123 | Contributions with other software (except as part of its Contributor 124 | Version); or 125 | 126 | (c) under Patent Claims infringed by Covered Software in the absence of 127 | its Contributions. 128 | 129 | This License does not grant any rights in the trademarks, service marks, 130 | or logos of any Contributor (except as may be necessary to comply with 131 | the notice requirements in Section 3.4). 132 | 133 | 2.4. Subsequent Licenses 134 | 135 | No Contributor makes additional grants as a result of Your choice to 136 | distribute the Covered Software under a subsequent version of this 137 | License (see Section 10.2) or under the terms of a Secondary License (if 138 | permitted under the terms of Section 3.3). 139 | 140 | 2.5. Representation 141 | 142 | Each Contributor represents that the Contributor believes its 143 | Contributions are its original creation(s) or it has sufficient rights 144 | to grant the rights to its Contributions conveyed by this License. 145 | 146 | 2.6. Fair Use 147 | 148 | This License is not intended to limit any rights You have under 149 | applicable copyright doctrines of fair use, fair dealing, or other 150 | equivalents. 151 | 152 | 2.7. Conditions 153 | 154 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted 155 | in Section 2.1. 156 | 157 | 3. Responsibilities 158 | ------------------- 159 | 160 | 3.1. Distribution of Source Form 161 | 162 | All distribution of Covered Software in Source Code Form, including any 163 | Modifications that You create or to which You contribute, must be under 164 | the terms of this License. You must inform recipients that the Source 165 | Code Form of the Covered Software is governed by the terms of this 166 | License, and how they can obtain a copy of this License. You may not 167 | attempt to alter or restrict the recipients' rights in the Source Code 168 | Form. 169 | 170 | 3.2. Distribution of Executable Form 171 | 172 | If You distribute Covered Software in Executable Form then: 173 | 174 | (a) such Covered Software must also be made available in Source Code 175 | Form, as described in Section 3.1, and You must inform recipients of 176 | the Executable Form how they can obtain a copy of such Source Code 177 | Form by reasonable means in a timely manner, at a charge no more 178 | than the cost of distribution to the recipient; and 179 | 180 | (b) You may distribute such Executable Form under the terms of this 181 | License, or sublicense it under different terms, provided that the 182 | license for the Executable Form does not attempt to limit or alter 183 | the recipients' rights in the Source Code Form under this License. 184 | 185 | 3.3. Distribution of a Larger Work 186 | 187 | You may create and distribute a Larger Work under terms of Your choice, 188 | provided that You also comply with the requirements of this License for 189 | the Covered Software. If the Larger Work is a combination of Covered 190 | Software with a work governed by one or more Secondary Licenses, and the 191 | Covered Software is not Incompatible With Secondary Licenses, this 192 | License permits You to additionally distribute such Covered Software 193 | under the terms of such Secondary License(s), so that the recipient of 194 | the Larger Work may, at their option, further distribute the Covered 195 | Software under the terms of either this License or such Secondary 196 | License(s). 197 | 198 | 3.4. Notices 199 | 200 | You may not remove or alter the substance of any license notices 201 | (including copyright notices, patent notices, disclaimers of warranty, 202 | or limitations of liability) contained within the Source Code Form of 203 | the Covered Software, except that You may alter any license notices to 204 | the extent required to remedy known factual inaccuracies. 205 | 206 | 3.5. Application of Additional Terms 207 | 208 | You may choose to offer, and to charge a fee for, warranty, support, 209 | indemnity or liability obligations to one or more recipients of Covered 210 | Software. However, You may do so only on Your own behalf, and not on 211 | behalf of any Contributor. You must make it absolutely clear that any 212 | such warranty, support, indemnity, or liability obligation is offered by 213 | You alone, and You hereby agree to indemnify every Contributor for any 214 | liability incurred by such Contributor as a result of warranty, support, 215 | indemnity or liability terms You offer. You may include additional 216 | disclaimers of warranty and limitations of liability specific to any 217 | jurisdiction. 218 | 219 | 4. Inability to Comply Due to Statute or Regulation 220 | --------------------------------------------------- 221 | 222 | If it is impossible for You to comply with any of the terms of this 223 | License with respect to some or all of the Covered Software due to 224 | statute, judicial order, or regulation then You must: (a) comply with 225 | the terms of this License to the maximum extent possible; and (b) 226 | describe the limitations and the code they affect. Such description must 227 | be placed in a text file included with all distributions of the Covered 228 | Software under this License. Except to the extent prohibited by statute 229 | or regulation, such description must be sufficiently detailed for a 230 | recipient of ordinary skill to be able to understand it. 231 | 232 | 5. Termination 233 | -------------- 234 | 235 | 5.1. The rights granted under this License will terminate automatically 236 | if You fail to comply with any of its terms. However, if You become 237 | compliant, then the rights granted under this License from a particular 238 | Contributor are reinstated (a) provisionally, unless and until such 239 | Contributor explicitly and finally terminates Your grants, and (b) on an 240 | ongoing basis, if such Contributor fails to notify You of the 241 | non-compliance by some reasonable means prior to 60 days after You have 242 | come back into compliance. Moreover, Your grants from a particular 243 | Contributor are reinstated on an ongoing basis if such Contributor 244 | notifies You of the non-compliance by some reasonable means, this is the 245 | first time You have received notice of non-compliance with this License 246 | from such Contributor, and You become compliant prior to 30 days after 247 | Your receipt of the notice. 248 | 249 | 5.2. If You initiate litigation against any entity by asserting a patent 250 | infringement claim (excluding declaratory judgment actions, 251 | counter-claims, and cross-claims) alleging that a Contributor Version 252 | directly or indirectly infringes any patent, then the rights granted to 253 | You by any and all Contributors for the Covered Software under Section 254 | 2.1 of this License shall terminate. 255 | 256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all 257 | end user license agreements (excluding distributors and resellers) which 258 | have been validly granted by You or Your distributors under this License 259 | prior to termination shall survive termination. 260 | 261 | ************************************************************************ 262 | * * 263 | * 6. Disclaimer of Warranty * 264 | * ------------------------- * 265 | * * 266 | * Covered Software is provided under this License on an "as is" * 267 | * basis, without warranty of any kind, either expressed, implied, or * 268 | * statutory, including, without limitation, warranties that the * 269 | * Covered Software is free of defects, merchantable, fit for a * 270 | * particular purpose or non-infringing. The entire risk as to the * 271 | * quality and performance of the Covered Software is with You. * 272 | * Should any Covered Software prove defective in any respect, You * 273 | * (not any Contributor) assume the cost of any necessary servicing, * 274 | * repair, or correction. This disclaimer of warranty constitutes an * 275 | * essential part of this License. No use of any Covered Software is * 276 | * authorized under this License except under this disclaimer. * 277 | * * 278 | ************************************************************************ 279 | 280 | ************************************************************************ 281 | * * 282 | * 7. Limitation of Liability * 283 | * -------------------------- * 284 | * * 285 | * Under no circumstances and under no legal theory, whether tort * 286 | * (including negligence), contract, or otherwise, shall any * 287 | * Contributor, or anyone who distributes Covered Software as * 288 | * permitted above, be liable to You for any direct, indirect, * 289 | * special, incidental, or consequential damages of any character * 290 | * including, without limitation, damages for lost profits, loss of * 291 | * goodwill, work stoppage, computer failure or malfunction, or any * 292 | * and all other commercial damages or losses, even if such party * 293 | * shall have been informed of the possibility of such damages. This * 294 | * limitation of liability shall not apply to liability for death or * 295 | * personal injury resulting from such party's negligence to the * 296 | * extent applicable law prohibits such limitation. Some * 297 | * jurisdictions do not allow the exclusion or limitation of * 298 | * incidental or consequential damages, so this exclusion and * 299 | * limitation may not apply to You. * 300 | * * 301 | ************************************************************************ 302 | 303 | 8. Litigation 304 | ------------- 305 | 306 | Any litigation relating to this License may be brought only in the 307 | courts of a jurisdiction where the defendant maintains its principal 308 | place of business and such litigation shall be governed by laws of that 309 | jurisdiction, without reference to its conflict-of-law provisions. 310 | Nothing in this Section shall prevent a party's ability to bring 311 | cross-claims or counter-claims. 312 | 313 | 9. Miscellaneous 314 | ---------------- 315 | 316 | This License represents the complete agreement concerning the subject 317 | matter hereof. If any provision of this License is held to be 318 | unenforceable, such provision shall be reformed only to the extent 319 | necessary to make it enforceable. Any law or regulation which provides 320 | that the language of a contract shall be construed against the drafter 321 | shall not be used to construe this License against a Contributor. 322 | 323 | 10. Versions of the License 324 | --------------------------- 325 | 326 | 10.1. New Versions 327 | 328 | Mozilla Foundation is the license steward. Except as provided in Section 329 | 10.3, no one other than the license steward has the right to modify or 330 | publish new versions of this License. Each version will be given a 331 | distinguishing version number. 332 | 333 | 10.2. Effect of New Versions 334 | 335 | You may distribute the Covered Software under the terms of the version 336 | of the License under which You originally received the Covered Software, 337 | or under the terms of any subsequent version published by the license 338 | steward. 339 | 340 | 10.3. Modified Versions 341 | 342 | If you create software not governed by this License, and you want to 343 | create a new license for such software, you may create and use a 344 | modified version of this License if you rename the license and remove 345 | any references to the name of the license steward (except to note that 346 | such modified license differs from this License). 347 | 348 | 10.4. Distributing Source Code Form that is Incompatible With Secondary 349 | Licenses 350 | 351 | If You choose to distribute Source Code Form that is Incompatible With 352 | Secondary Licenses under the terms of this version of the License, the 353 | notice described in Exhibit B of this License must be attached. 354 | 355 | Exhibit A - Source Code Form License Notice 356 | ------------------------------------------- 357 | 358 | This Source Code Form is subject to the terms of the Mozilla Public 359 | License, v. 2.0. If a copy of the MPL was not distributed with this 360 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 361 | 362 | If it is not possible or desirable to put the notice in a particular 363 | file, then You may include the notice in a location (such as a LICENSE 364 | file in a relevant directory) where a recipient would be likely to look 365 | for such a notice. 366 | 367 | You may add additional accurate notices of copyright ownership. 368 | 369 | Exhibit B - "Incompatible With Secondary Licenses" Notice 370 | --------------------------------------------------------- 371 | 372 | This Source Code Form is "Incompatible With Secondary Licenses", as 373 | defined by the Mozilla Public License, v. 2.0. 374 | -------------------------------------------------------------------------------- /rabbitmq-components.mk: -------------------------------------------------------------------------------- 1 | ifeq ($(.DEFAULT_GOAL),) 2 | # Define default goal to `all` because this file defines some targets 3 | # before the inclusion of erlang.mk leading to the wrong target becoming 4 | # the default. 5 | .DEFAULT_GOAL = all 6 | endif 7 | 8 | # PROJECT_VERSION defaults to: 9 | # 1. the version exported by rabbitmq-server-release; 10 | # 2. the version stored in `git-revisions.txt`, if it exists; 11 | # 3. a version based on git-describe(1), if it is a Git clone; 12 | # 4. 0.0.0 13 | 14 | PROJECT_VERSION := $(RABBITMQ_VERSION) 15 | 16 | ifeq ($(PROJECT_VERSION),) 17 | PROJECT_VERSION := $(shell \ 18 | if test -f git-revisions.txt; then \ 19 | head -n1 git-revisions.txt | \ 20 | awk '{print $$$(words $(PROJECT_DESCRIPTION) version);}'; \ 21 | else \ 22 | (git describe --dirty --abbrev=7 --tags --always --first-parent \ 23 | 2>/dev/null || echo rabbitmq_v0_0_0) | \ 24 | sed -e 's/^rabbitmq_v//' -e 's/^v//' -e 's/_/./g' -e 's/-/+/' \ 25 | -e 's/-/./g'; \ 26 | fi) 27 | endif 28 | 29 | # -------------------------------------------------------------------- 30 | # RabbitMQ components. 31 | # -------------------------------------------------------------------- 32 | 33 | # For RabbitMQ repositories, we want to checkout branches which match 34 | # the parent project. For instance, if the parent project is on a 35 | # release tag, dependencies must be on the same release tag. If the 36 | # parent project is on a topic branch, dependencies must be on the same 37 | # topic branch or fallback to `stable` or `master` whichever was the 38 | # base of the topic branch. 39 | 40 | dep_amqp_client = git_rmq rabbitmq-erlang-client $(current_rmq_ref) $(base_rmq_ref) master 41 | dep_amqp10_client = git_rmq rabbitmq-amqp1.0-client $(current_rmq_ref) $(base_rmq_ref) master 42 | dep_amqp10_common = git_rmq rabbitmq-amqp1.0-common $(current_rmq_ref) $(base_rmq_ref) master 43 | dep_rabbit = git_rmq rabbitmq-server $(current_rmq_ref) $(base_rmq_ref) master 44 | dep_rabbit_common = git_rmq rabbitmq-common $(current_rmq_ref) $(base_rmq_ref) master 45 | dep_rabbitmq_amqp1_0 = git_rmq rabbitmq-amqp1.0 $(current_rmq_ref) $(base_rmq_ref) master 46 | dep_rabbitmq_auth_backend_amqp = git_rmq rabbitmq-auth-backend-amqp $(current_rmq_ref) $(base_rmq_ref) master 47 | dep_rabbitmq_auth_backend_cache = git_rmq rabbitmq-auth-backend-cache $(current_rmq_ref) $(base_rmq_ref) master 48 | dep_rabbitmq_auth_backend_http = git_rmq rabbitmq-auth-backend-http $(current_rmq_ref) $(base_rmq_ref) master 49 | dep_rabbitmq_auth_backend_ldap = git_rmq rabbitmq-auth-backend-ldap $(current_rmq_ref) $(base_rmq_ref) master 50 | dep_rabbitmq_auth_backend_oauth2 = git_rmq rabbitmq-auth-backend-oauth2 $(current_rmq_ref) $(base_rmq_ref) master 51 | dep_rabbitmq_auth_mechanism_ssl = git_rmq rabbitmq-auth-mechanism-ssl $(current_rmq_ref) $(base_rmq_ref) master 52 | dep_rabbitmq_aws = git_rmq rabbitmq-aws $(current_rmq_ref) $(base_rmq_ref) master 53 | dep_rabbitmq_boot_steps_visualiser = git_rmq rabbitmq-boot-steps-visualiser $(current_rmq_ref) $(base_rmq_ref) master 54 | dep_rabbitmq_cli = git_rmq rabbitmq-cli $(current_rmq_ref) $(base_rmq_ref) master 55 | dep_rabbitmq_codegen = git_rmq rabbitmq-codegen $(current_rmq_ref) $(base_rmq_ref) master 56 | dep_rabbitmq_consistent_hash_exchange = git_rmq rabbitmq-consistent-hash-exchange $(current_rmq_ref) $(base_rmq_ref) master 57 | dep_rabbitmq_ct_client_helpers = git_rmq rabbitmq-ct-client-helpers $(current_rmq_ref) $(base_rmq_ref) master 58 | dep_rabbitmq_ct_helpers = git_rmq rabbitmq-ct-helpers $(current_rmq_ref) $(base_rmq_ref) master 59 | dep_rabbitmq_delayed_message_exchange = git_rmq rabbitmq-delayed-message-exchange $(current_rmq_ref) $(base_rmq_ref) master 60 | dep_rabbitmq_dotnet_client = git_rmq rabbitmq-dotnet-client $(current_rmq_ref) $(base_rmq_ref) master 61 | dep_rabbitmq_event_exchange = git_rmq rabbitmq-event-exchange $(current_rmq_ref) $(base_rmq_ref) master 62 | dep_rabbitmq_federation = git_rmq rabbitmq-federation $(current_rmq_ref) $(base_rmq_ref) master 63 | dep_rabbitmq_federation_management = git_rmq rabbitmq-federation-management $(current_rmq_ref) $(base_rmq_ref) master 64 | dep_rabbitmq_java_client = git_rmq rabbitmq-java-client $(current_rmq_ref) $(base_rmq_ref) master 65 | dep_rabbitmq_jms_client = git_rmq rabbitmq-jms-client $(current_rmq_ref) $(base_rmq_ref) master 66 | dep_rabbitmq_jms_cts = git_rmq rabbitmq-jms-cts $(current_rmq_ref) $(base_rmq_ref) master 67 | dep_rabbitmq_jms_topic_exchange = git_rmq rabbitmq-jms-topic-exchange $(current_rmq_ref) $(base_rmq_ref) master 68 | dep_rabbitmq_lvc_exchange = git_rmq rabbitmq-lvc-exchange $(current_rmq_ref) $(base_rmq_ref) master 69 | dep_rabbitmq_management = git_rmq rabbitmq-management $(current_rmq_ref) $(base_rmq_ref) master 70 | dep_rabbitmq_management_agent = git_rmq rabbitmq-management-agent $(current_rmq_ref) $(base_rmq_ref) master 71 | dep_rabbitmq_management_exchange = git_rmq rabbitmq-management-exchange $(current_rmq_ref) $(base_rmq_ref) master 72 | dep_rabbitmq_management_themes = git_rmq rabbitmq-management-themes $(current_rmq_ref) $(base_rmq_ref) master 73 | dep_rabbitmq_message_timestamp = git_rmq rabbitmq-message-timestamp $(current_rmq_ref) $(base_rmq_ref) master 74 | dep_rabbitmq_metronome = git_rmq rabbitmq-metronome $(current_rmq_ref) $(base_rmq_ref) master 75 | dep_rabbitmq_mqtt = git_rmq rabbitmq-mqtt $(current_rmq_ref) $(base_rmq_ref) master 76 | dep_rabbitmq_objc_client = git_rmq rabbitmq-objc-client $(current_rmq_ref) $(base_rmq_ref) master 77 | dep_rabbitmq_peer_discovery_aws = git_rmq rabbitmq-peer-discovery-aws $(current_rmq_ref) $(base_rmq_ref) master 78 | dep_rabbitmq_peer_discovery_common = git_rmq rabbitmq-peer-discovery-common $(current_rmq_ref) $(base_rmq_ref) master 79 | dep_rabbitmq_peer_discovery_consul = git_rmq rabbitmq-peer-discovery-consul $(current_rmq_ref) $(base_rmq_ref) master 80 | dep_rabbitmq_peer_discovery_etcd = git_rmq rabbitmq-peer-discovery-etcd $(current_rmq_ref) $(base_rmq_ref) master 81 | dep_rabbitmq_peer_discovery_k8s = git_rmq rabbitmq-peer-discovery-k8s $(current_rmq_ref) $(base_rmq_ref) master 82 | dep_rabbitmq_prometheus = git_rmq rabbitmq-prometheus $(current_rmq_ref) $(base_rmq_ref) master 83 | dep_rabbitmq_random_exchange = git_rmq rabbitmq-random-exchange $(current_rmq_ref) $(base_rmq_ref) master 84 | dep_rabbitmq_recent_history_exchange = git_rmq rabbitmq-recent-history-exchange $(current_rmq_ref) $(base_rmq_ref) master 85 | dep_rabbitmq_routing_node_stamp = git_rmq rabbitmq-routing-node-stamp $(current_rmq_ref) $(base_rmq_ref) master 86 | dep_rabbitmq_rtopic_exchange = git_rmq rabbitmq-rtopic-exchange $(current_rmq_ref) $(base_rmq_ref) master 87 | dep_rabbitmq_server_release = git_rmq rabbitmq-server-release $(current_rmq_ref) $(base_rmq_ref) master 88 | dep_rabbitmq_sharding = git_rmq rabbitmq-sharding $(current_rmq_ref) $(base_rmq_ref) master 89 | dep_rabbitmq_shovel = git_rmq rabbitmq-shovel $(current_rmq_ref) $(base_rmq_ref) master 90 | dep_rabbitmq_shovel_management = git_rmq rabbitmq-shovel-management $(current_rmq_ref) $(base_rmq_ref) master 91 | dep_rabbitmq_stomp = git_rmq rabbitmq-stomp $(current_rmq_ref) $(base_rmq_ref) master 92 | dep_rabbitmq_stream = git_rmq rabbitmq-stream $(current_rmq_ref) $(base_rmq_ref) master 93 | dep_rabbitmq_toke = git_rmq rabbitmq-toke $(current_rmq_ref) $(base_rmq_ref) master 94 | dep_rabbitmq_top = git_rmq rabbitmq-top $(current_rmq_ref) $(base_rmq_ref) master 95 | dep_rabbitmq_tracing = git_rmq rabbitmq-tracing $(current_rmq_ref) $(base_rmq_ref) master 96 | dep_rabbitmq_trust_store = git_rmq rabbitmq-trust-store $(current_rmq_ref) $(base_rmq_ref) master 97 | dep_rabbitmq_test = git_rmq rabbitmq-test $(current_rmq_ref) $(base_rmq_ref) master 98 | dep_rabbitmq_web_dispatch = git_rmq rabbitmq-web-dispatch $(current_rmq_ref) $(base_rmq_ref) master 99 | dep_rabbitmq_web_stomp = git_rmq rabbitmq-web-stomp $(current_rmq_ref) $(base_rmq_ref) master 100 | dep_rabbitmq_web_stomp_examples = git_rmq rabbitmq-web-stomp-examples $(current_rmq_ref) $(base_rmq_ref) master 101 | dep_rabbitmq_web_mqtt = git_rmq rabbitmq-web-mqtt $(current_rmq_ref) $(base_rmq_ref) master 102 | dep_rabbitmq_web_mqtt_examples = git_rmq rabbitmq-web-mqtt-examples $(current_rmq_ref) $(base_rmq_ref) master 103 | dep_rabbitmq_website = git_rmq rabbitmq-website $(current_rmq_ref) $(base_rmq_ref) live master 104 | dep_toke = git_rmq toke $(current_rmq_ref) $(base_rmq_ref) master 105 | 106 | dep_rabbitmq_public_umbrella = git_rmq rabbitmq-public-umbrella $(current_rmq_ref) $(base_rmq_ref) master 107 | 108 | # Third-party dependencies version pinning. 109 | # 110 | # We do that in this file, which is copied in all projects, to ensure 111 | # all projects use the same versions. It avoids conflicts and makes it 112 | # possible to work with rabbitmq-public-umbrella. 113 | 114 | dep_accept = hex 0.3.5 115 | dep_cowboy = hex 2.8.0 116 | dep_cowlib = hex 2.9.1 117 | dep_jsx = hex 2.11.0 118 | dep_lager = hex 3.8.0 119 | dep_prometheus = git https://github.com/deadtrickster/prometheus.erl.git master 120 | dep_ra = git https://github.com/rabbitmq/ra.git master 121 | dep_ranch = hex 1.7.1 122 | dep_recon = hex 2.5.1 123 | dep_observer_cli = hex 1.5.4 124 | dep_stdout_formatter = hex 0.2.4 125 | dep_sysmon_handler = hex 1.3.0 126 | 127 | RABBITMQ_COMPONENTS = amqp_client \ 128 | amqp10_common \ 129 | amqp10_client \ 130 | rabbit \ 131 | rabbit_common \ 132 | rabbitmq_amqp1_0 \ 133 | rabbitmq_auth_backend_amqp \ 134 | rabbitmq_auth_backend_cache \ 135 | rabbitmq_auth_backend_http \ 136 | rabbitmq_auth_backend_ldap \ 137 | rabbitmq_auth_backend_oauth2 \ 138 | rabbitmq_auth_mechanism_ssl \ 139 | rabbitmq_aws \ 140 | rabbitmq_boot_steps_visualiser \ 141 | rabbitmq_cli \ 142 | rabbitmq_codegen \ 143 | rabbitmq_consistent_hash_exchange \ 144 | rabbitmq_ct_client_helpers \ 145 | rabbitmq_ct_helpers \ 146 | rabbitmq_delayed_message_exchange \ 147 | rabbitmq_dotnet_client \ 148 | rabbitmq_event_exchange \ 149 | rabbitmq_federation \ 150 | rabbitmq_federation_management \ 151 | rabbitmq_java_client \ 152 | rabbitmq_jms_client \ 153 | rabbitmq_jms_cts \ 154 | rabbitmq_jms_topic_exchange \ 155 | rabbitmq_lvc_exchange \ 156 | rabbitmq_management \ 157 | rabbitmq_management_agent \ 158 | rabbitmq_management_exchange \ 159 | rabbitmq_management_themes \ 160 | rabbitmq_message_timestamp \ 161 | rabbitmq_metronome \ 162 | rabbitmq_mqtt \ 163 | rabbitmq_objc_client \ 164 | rabbitmq_peer_discovery_aws \ 165 | rabbitmq_peer_discovery_common \ 166 | rabbitmq_peer_discovery_consul \ 167 | rabbitmq_peer_discovery_etcd \ 168 | rabbitmq_peer_discovery_k8s \ 169 | rabbitmq_prometheus \ 170 | rabbitmq_random_exchange \ 171 | rabbitmq_recent_history_exchange \ 172 | rabbitmq_routing_node_stamp \ 173 | rabbitmq_rtopic_exchange \ 174 | rabbitmq_server_release \ 175 | rabbitmq_sharding \ 176 | rabbitmq_shovel \ 177 | rabbitmq_shovel_management \ 178 | rabbitmq_stomp \ 179 | rabbitmq_stream \ 180 | rabbitmq_toke \ 181 | rabbitmq_top \ 182 | rabbitmq_tracing \ 183 | rabbitmq_trust_store \ 184 | rabbitmq_web_dispatch \ 185 | rabbitmq_web_mqtt \ 186 | rabbitmq_web_mqtt_examples \ 187 | rabbitmq_web_stomp \ 188 | rabbitmq_web_stomp_examples \ 189 | rabbitmq_website 190 | 191 | # Erlang.mk does not rebuild dependencies by default, once they were 192 | # compiled once, except for those listed in the `$(FORCE_REBUILD)` 193 | # variable. 194 | # 195 | # We want all RabbitMQ components to always be rebuilt: this eases 196 | # the work on several components at the same time. 197 | 198 | FORCE_REBUILD = $(RABBITMQ_COMPONENTS) 199 | 200 | # Several components have a custom erlang.mk/build.config, mainly 201 | # to disable eunit. Therefore, we can't use the top-level project's 202 | # erlang.mk copy. 203 | NO_AUTOPATCH += $(RABBITMQ_COMPONENTS) 204 | 205 | ifeq ($(origin current_rmq_ref),undefined) 206 | ifneq ($(wildcard .git),) 207 | current_rmq_ref := $(shell (\ 208 | ref=$$(LANG=C git branch --list | awk '/^\* \(.*detached / {ref=$$0; sub(/.*detached [^ ]+ /, "", ref); sub(/\)$$/, "", ref); print ref; exit;} /^\* / {ref=$$0; sub(/^\* /, "", ref); print ref; exit}');\ 209 | if test "$$(git rev-parse --short HEAD)" != "$$ref"; then echo "$$ref"; fi)) 210 | else 211 | current_rmq_ref := master 212 | endif 213 | endif 214 | export current_rmq_ref 215 | 216 | ifeq ($(origin base_rmq_ref),undefined) 217 | ifneq ($(wildcard .git),) 218 | possible_base_rmq_ref := master 219 | ifeq ($(possible_base_rmq_ref),$(current_rmq_ref)) 220 | base_rmq_ref := $(current_rmq_ref) 221 | else 222 | base_rmq_ref := $(shell \ 223 | (git rev-parse --verify -q master >/dev/null && \ 224 | git rev-parse --verify -q $(possible_base_rmq_ref) >/dev/null && \ 225 | git merge-base --is-ancestor $$(git merge-base master HEAD) $(possible_base_rmq_ref) && \ 226 | echo $(possible_base_rmq_ref)) || \ 227 | echo master) 228 | endif 229 | else 230 | base_rmq_ref := master 231 | endif 232 | endif 233 | export base_rmq_ref 234 | 235 | # Repository URL selection. 236 | # 237 | # First, we infer other components' location from the current project 238 | # repository URL, if it's a Git repository: 239 | # - We take the "origin" remote URL as the base 240 | # - The current project name and repository name is replaced by the 241 | # target's properties: 242 | # eg. rabbitmq-common is replaced by rabbitmq-codegen 243 | # eg. rabbit_common is replaced by rabbitmq_codegen 244 | # 245 | # If cloning from this computed location fails, we fallback to RabbitMQ 246 | # upstream which is GitHub. 247 | 248 | # Macro to transform eg. "rabbit_common" to "rabbitmq-common". 249 | rmq_cmp_repo_name = $(word 2,$(dep_$(1))) 250 | 251 | # Upstream URL for the current project. 252 | RABBITMQ_COMPONENT_REPO_NAME := $(call rmq_cmp_repo_name,$(PROJECT)) 253 | RABBITMQ_UPSTREAM_FETCH_URL ?= https://github.com/rabbitmq/$(RABBITMQ_COMPONENT_REPO_NAME).git 254 | RABBITMQ_UPSTREAM_PUSH_URL ?= git@github.com:rabbitmq/$(RABBITMQ_COMPONENT_REPO_NAME).git 255 | 256 | # Current URL for the current project. If this is not a Git clone, 257 | # default to the upstream Git repository. 258 | ifneq ($(wildcard .git),) 259 | git_origin_fetch_url := $(shell git config remote.origin.url) 260 | git_origin_push_url := $(shell git config remote.origin.pushurl || git config remote.origin.url) 261 | RABBITMQ_CURRENT_FETCH_URL ?= $(git_origin_fetch_url) 262 | RABBITMQ_CURRENT_PUSH_URL ?= $(git_origin_push_url) 263 | else 264 | RABBITMQ_CURRENT_FETCH_URL ?= $(RABBITMQ_UPSTREAM_FETCH_URL) 265 | RABBITMQ_CURRENT_PUSH_URL ?= $(RABBITMQ_UPSTREAM_PUSH_URL) 266 | endif 267 | 268 | # Macro to replace the following pattern: 269 | # 1. /foo.git -> /bar.git 270 | # 2. /foo -> /bar 271 | # 3. /foo/ -> /bar/ 272 | subst_repo_name = $(patsubst %/$(1)/%,%/$(2)/%,$(patsubst %/$(1),%/$(2),$(patsubst %/$(1).git,%/$(2).git,$(3)))) 273 | 274 | # Macro to replace both the project's name (eg. "rabbit_common") and 275 | # repository name (eg. "rabbitmq-common") by the target's equivalent. 276 | # 277 | # This macro is kept on one line because we don't want whitespaces in 278 | # the returned value, as it's used in $(dep_fetch_git_rmq) in a shell 279 | # single-quoted string. 280 | dep_rmq_repo = $(if $(dep_$(2)),$(call subst_repo_name,$(PROJECT),$(2),$(call subst_repo_name,$(RABBITMQ_COMPONENT_REPO_NAME),$(call rmq_cmp_repo_name,$(2)),$(1))),$(pkg_$(1)_repo)) 281 | 282 | dep_rmq_commits = $(if $(dep_$(1)), \ 283 | $(wordlist 3,$(words $(dep_$(1))),$(dep_$(1))), \ 284 | $(pkg_$(1)_commit)) 285 | 286 | define dep_fetch_git_rmq 287 | fetch_url1='$(call dep_rmq_repo,$(RABBITMQ_CURRENT_FETCH_URL),$(1))'; \ 288 | fetch_url2='$(call dep_rmq_repo,$(RABBITMQ_UPSTREAM_FETCH_URL),$(1))'; \ 289 | if test "$$$$fetch_url1" != '$(RABBITMQ_CURRENT_FETCH_URL)' && \ 290 | git clone -q -n -- "$$$$fetch_url1" $(DEPS_DIR)/$(call dep_name,$(1)); then \ 291 | fetch_url="$$$$fetch_url1"; \ 292 | push_url='$(call dep_rmq_repo,$(RABBITMQ_CURRENT_PUSH_URL),$(1))'; \ 293 | elif git clone -q -n -- "$$$$fetch_url2" $(DEPS_DIR)/$(call dep_name,$(1)); then \ 294 | fetch_url="$$$$fetch_url2"; \ 295 | push_url='$(call dep_rmq_repo,$(RABBITMQ_UPSTREAM_PUSH_URL),$(1))'; \ 296 | fi; \ 297 | cd $(DEPS_DIR)/$(call dep_name,$(1)) && ( \ 298 | $(foreach ref,$(call dep_rmq_commits,$(1)), \ 299 | git checkout -q $(ref) >/dev/null 2>&1 || \ 300 | ) \ 301 | (echo "error: no valid pathspec among: $(call dep_rmq_commits,$(1))" \ 302 | 1>&2 && false) ) && \ 303 | (test "$$$$fetch_url" = "$$$$push_url" || \ 304 | git remote set-url --push origin "$$$$push_url") 305 | endef 306 | 307 | # -------------------------------------------------------------------- 308 | # Component distribution. 309 | # -------------------------------------------------------------------- 310 | 311 | list-dist-deps:: 312 | @: 313 | 314 | prepare-dist:: 315 | @: 316 | 317 | # -------------------------------------------------------------------- 318 | # Umbrella-specific settings. 319 | # -------------------------------------------------------------------- 320 | 321 | # If the top-level project is a RabbitMQ component, we override 322 | # $(DEPS_DIR) for this project to point to the top-level's one. 323 | # 324 | # We also verify that the guessed DEPS_DIR is actually named `deps`, 325 | # to rule out any situation where it is a coincidence that we found a 326 | # `rabbitmq-components.mk` up upper directories. 327 | 328 | possible_deps_dir_1 = $(abspath ..) 329 | possible_deps_dir_2 = $(abspath ../../..) 330 | 331 | ifeq ($(notdir $(possible_deps_dir_1)),deps) 332 | ifneq ($(wildcard $(possible_deps_dir_1)/../rabbitmq-components.mk),) 333 | deps_dir_overriden = 1 334 | DEPS_DIR ?= $(possible_deps_dir_1) 335 | DISABLE_DISTCLEAN = 1 336 | endif 337 | endif 338 | 339 | ifeq ($(deps_dir_overriden),) 340 | ifeq ($(notdir $(possible_deps_dir_2)),deps) 341 | ifneq ($(wildcard $(possible_deps_dir_2)/../rabbitmq-components.mk),) 342 | deps_dir_overriden = 1 343 | DEPS_DIR ?= $(possible_deps_dir_2) 344 | DISABLE_DISTCLEAN = 1 345 | endif 346 | endif 347 | endif 348 | 349 | ifneq ($(wildcard UMBRELLA.md),) 350 | DISABLE_DISTCLEAN = 1 351 | endif 352 | 353 | # We disable `make distclean` so $(DEPS_DIR) is not accidentally removed. 354 | 355 | ifeq ($(DISABLE_DISTCLEAN),1) 356 | ifneq ($(filter distclean distclean-deps,$(MAKECMDGOALS)),) 357 | SKIP_DEPS = 1 358 | endif 359 | endif 360 | -------------------------------------------------------------------------------- /src/amqp10_client_connection.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(amqp10_client_connection). 9 | 10 | -behaviour(gen_statem). 11 | 12 | -include("amqp10_client.hrl"). 13 | -include_lib("amqp10_common/include/amqp10_framing.hrl"). 14 | 15 | -ifdef(nowarn_deprecated_gen_fsm). 16 | -compile({nowarn_deprecated_function, 17 | [{gen_fsm, reply, 2}, 18 | {gen_fsm, send_all_state_event, 2}, 19 | {gen_fsm, send_event, 2}, 20 | {gen_fsm, start_link, 3}, 21 | {gen_fsm, sync_send_all_state_event, 2}]}). 22 | -endif. 23 | 24 | %% Public API. 25 | -export([open/1, 26 | close/2]). 27 | 28 | %% Private API. 29 | -export([start_link/2, 30 | socket_ready/2, 31 | protocol_header_received/5, 32 | begin_session/1, 33 | heartbeat/1]). 34 | 35 | %% gen_fsm callbacks. 36 | -export([init/1, 37 | callback_mode/0, 38 | terminate/3, 39 | code_change/4]). 40 | 41 | %% gen_fsm state callbacks. 42 | -export([expecting_socket/3, 43 | sasl_hdr_sent/3, 44 | sasl_hdr_rcvds/3, 45 | sasl_init_sent/3, 46 | hdr_sent/3, 47 | open_sent/3, 48 | opened/3, 49 | close_sent/3]). 50 | 51 | -type amqp10_socket() :: {tcp, gen_tcp:socket()} | {ssl, ssl:sslsocket()}. 52 | 53 | -type milliseconds() :: non_neg_integer(). 54 | 55 | -type address() :: inet:socket_address() | inet:hostname(). 56 | 57 | -type connection_config() :: 58 | #{container_id => binary(), % AMQP container id 59 | hostname => binary(), % the dns name of the target host 60 | addresses => [address()], 61 | address => address(), 62 | port => inet:port_number(), 63 | tls_opts => {secure_port, [ssl:ssl_option()]}, 64 | notify => pid() | none, % the pid to send connection events to 65 | notify_when_opened => pid() | none, 66 | notify_when_closed => pid() | none, 67 | max_frame_size => non_neg_integer(), % TODO: constrain to large than 512 68 | outgoing_max_frame_size => non_neg_integer() | undefined, 69 | idle_time_out => milliseconds(), 70 | % set to a negative value to allow a sender to "overshoot" the flow 71 | % control by this margin 72 | transfer_limit_margin => 0 | neg_integer(), 73 | sasl => none | anon | {plain, User :: binary(), Pwd :: binary()}, 74 | notify => pid(), 75 | notify_when_opened => pid() | none, 76 | notify_when_closed => pid() | none 77 | }. 78 | 79 | -record(state, 80 | {next_channel = 1 :: pos_integer(), 81 | connection_sup :: pid(), 82 | reader_m_ref :: reference() | undefined, 83 | sessions_sup :: pid() | undefined, 84 | pending_session_reqs = [] :: [term()], 85 | reader :: pid() | undefined, 86 | socket :: amqp10_socket() | undefined, 87 | idle_time_out :: non_neg_integer() | undefined, 88 | heartbeat_timer :: timer:tref() | undefined, 89 | config :: connection_config() 90 | }). 91 | 92 | -export_type([connection_config/0, 93 | amqp10_socket/0]). 94 | 95 | -define(DEFAULT_TIMEOUT, 5000). 96 | 97 | %% ------------------------------------------------------------------- 98 | %% Public API. 99 | %% ------------------------------------------------------------------- 100 | 101 | -spec open(connection_config()) -> supervisor:startchild_ret(). 102 | open(Config) -> 103 | %% Start the supervision tree dedicated to that connection. It 104 | %% starts at least a connection process (the PID we want to return) 105 | %% and a reader process (responsible for opening and reading the 106 | %% socket). 107 | case supervisor:start_child(amqp10_client_sup, [Config]) of 108 | {ok, ConnSup} -> 109 | %% We query the PIDs of the connection and reader processes. The 110 | %% reader process needs to know the connection PID to send it the 111 | %% socket. 112 | Children = supervisor:which_children(ConnSup), 113 | {_, Reader, _, _} = lists:keyfind(reader, 1, Children), 114 | {_, Connection, _, _} = lists:keyfind(connection, 1, Children), 115 | {_, SessionsSup, _, _} = lists:keyfind(sessions, 1, Children), 116 | set_other_procs(Connection, #{sessions_sup => SessionsSup, 117 | reader => Reader}), 118 | {ok, Connection}; 119 | Error -> 120 | Error 121 | end. 122 | 123 | -spec close(pid(), {amqp10_client_types:amqp_error() 124 | | amqp10_client_types:connection_error(), binary()} | none) -> ok. 125 | close(Pid, Reason) -> 126 | gen_statem:cast(Pid, {close, Reason}). 127 | 128 | %% ------------------------------------------------------------------- 129 | %% Private API. 130 | %% ------------------------------------------------------------------- 131 | 132 | start_link(Sup, Config) -> 133 | gen_statem:start_link(?MODULE, [Sup, Config], []). 134 | 135 | set_other_procs(Pid, OtherProcs) -> 136 | gen_statem:cast(Pid, {set_other_procs, OtherProcs}). 137 | 138 | -spec socket_ready(pid(), amqp10_socket()) -> ok. 139 | socket_ready(Pid, Socket) -> 140 | gen_statem:cast(Pid, {socket_ready, Socket}). 141 | 142 | -spec protocol_header_received(pid(), 0 | 3, non_neg_integer(), 143 | non_neg_integer(), non_neg_integer()) -> ok. 144 | protocol_header_received(Pid, Protocol, Maj, Min, Rev) -> 145 | gen_statem:cast(Pid, {protocol_header_received, Protocol, Maj, Min, Rev}). 146 | 147 | -spec begin_session(pid()) -> supervisor:startchild_ret(). 148 | begin_session(Pid) -> 149 | gen_statem:call(Pid, begin_session, {dirty_timeout, ?TIMEOUT}). 150 | 151 | heartbeat(Pid) -> 152 | gen_statem:cast(Pid, heartbeat). 153 | 154 | %% ------------------------------------------------------------------- 155 | %% gen_fsm callbacks. 156 | %% ------------------------------------------------------------------- 157 | 158 | callback_mode() -> [state_functions]. 159 | 160 | init([Sup, Config0]) -> 161 | process_flag(trap_exit, true), 162 | Config = maps:merge(config_defaults(), Config0), 163 | {ok, expecting_socket, #state{connection_sup = Sup, 164 | config = Config}}. 165 | 166 | expecting_socket(_EvtType, {socket_ready, Socket}, 167 | State = #state{config = Cfg}) -> 168 | State1 = State#state{socket = Socket}, 169 | case Cfg of 170 | #{sasl := none} -> 171 | ok = socket_send(Socket, ?AMQP_PROTOCOL_HEADER), 172 | {next_state, hdr_sent, State1}; 173 | _ -> 174 | ok = socket_send(Socket, ?SASL_PROTOCOL_HEADER), 175 | {next_state, sasl_hdr_sent, State1} 176 | end; 177 | expecting_socket(_EvtType, {set_other_procs, OtherProcs}, State) -> 178 | {keep_state, set_other_procs0(OtherProcs, State)}; 179 | expecting_socket({call, From}, begin_session, 180 | #state{pending_session_reqs = PendingSessionReqs} = State) -> 181 | %% The caller already asked for a new session but the connection 182 | %% isn't fully opened. Let's queue this request until the connection 183 | %% is ready. 184 | State1 = State#state{pending_session_reqs = [From | PendingSessionReqs]}, 185 | {keep_state, State1}. 186 | 187 | sasl_hdr_sent(_EvtType, {protocol_header_received, 3, 1, 0, 0}, State) -> 188 | {next_state, sasl_hdr_rcvds, State}; 189 | sasl_hdr_sent({call, From}, begin_session, 190 | #state{pending_session_reqs = PendingSessionReqs} = State) -> 191 | State1 = State#state{pending_session_reqs = [From | PendingSessionReqs]}, 192 | {keep_state, State1}. 193 | 194 | sasl_hdr_rcvds(_EvtType, #'v1_0.sasl_mechanisms'{ 195 | sasl_server_mechanisms = {array, symbol, Mechs}}, 196 | State = #state{config = #{sasl := Sasl}}) -> 197 | SaslBin = {symbol, sasl_to_bin(Sasl)}, 198 | case lists:any(fun(S) when S =:= SaslBin -> true; 199 | (_) -> false 200 | end, Mechs) of 201 | true -> 202 | ok = send_sasl_init(State, Sasl), 203 | {next_state, sasl_init_sent, State}; 204 | false -> 205 | {stop, {sasl_not_supported, Sasl}, State} 206 | end; 207 | sasl_hdr_rcvds({call, From}, begin_session, 208 | #state{pending_session_reqs = PendingSessionReqs} = State) -> 209 | State1 = State#state{pending_session_reqs = [From | PendingSessionReqs]}, 210 | {keep_state, State1}. 211 | 212 | sasl_init_sent(_EvtType, #'v1_0.sasl_outcome'{code = {ubyte, 0}}, 213 | #state{socket = Socket} = State) -> 214 | ok = socket_send(Socket, ?AMQP_PROTOCOL_HEADER), 215 | {next_state, hdr_sent, State}; 216 | sasl_init_sent(_EvtType, #'v1_0.sasl_outcome'{code = {ubyte, C}}, 217 | #state{} = State) when C==1;C==2;C==3;C==4 -> 218 | {stop, sasl_auth_failure, State}; 219 | sasl_init_sent({call, From}, begin_session, 220 | #state{pending_session_reqs = PendingSessionReqs} = State) -> 221 | State1 = State#state{pending_session_reqs = [From | PendingSessionReqs]}, 222 | {keep_state, State1}. 223 | 224 | hdr_sent(_EvtType, {protocol_header_received, 0, 1, 0, 0}, State) -> 225 | case send_open(State) of 226 | ok -> {next_state, open_sent, State}; 227 | Error -> {stop, Error, State} 228 | end; 229 | hdr_sent(_EvtType, {protocol_header_received, Protocol, Maj, Min, 230 | Rev}, State) -> 231 | error_logger:warning_msg("Unsupported protocol version: ~b ~b.~b.~b~n", 232 | [Protocol, Maj, Min, Rev]), 233 | {stop, normal, State}; 234 | hdr_sent({call, From}, begin_session, 235 | #state{pending_session_reqs = PendingSessionReqs} = State) -> 236 | State1 = State#state{pending_session_reqs = [From | PendingSessionReqs]}, 237 | {keep_state, State1}. 238 | 239 | open_sent(_EvtType, #'v1_0.open'{max_frame_size = MFSz, 240 | idle_time_out = Timeout}, 241 | #state{pending_session_reqs = PendingSessionReqs, 242 | config = Config} = State0) -> 243 | State = case Timeout of 244 | undefined -> State0; 245 | {uint, T} when T > 0 -> 246 | {ok, Tmr} = start_heartbeat_timer(T div 2), 247 | State0#state{idle_time_out = T div 2, 248 | heartbeat_timer = Tmr}; 249 | _ -> State0 250 | end, 251 | State1 = State#state{config = 252 | Config#{outgoing_max_frame_size => unpack(MFSz)}}, 253 | State2 = lists:foldr( 254 | fun(From, S0) -> 255 | {Ret, S2} = handle_begin_session(From, S0), 256 | _ = gen_statem:reply(From, Ret), 257 | S2 258 | end, State1, PendingSessionReqs), 259 | ok = notify_opened(Config), 260 | {next_state, opened, State2}; 261 | open_sent({call, From}, begin_session, 262 | #state{pending_session_reqs = PendingSessionReqs} = State) -> 263 | State1 = State#state{pending_session_reqs = [From | PendingSessionReqs]}, 264 | {keep_state, State1}. 265 | 266 | opened(_EvtType, heartbeat, State = #state{idle_time_out = T}) -> 267 | ok = send_heartbeat(State), 268 | {ok, Tmr} = start_heartbeat_timer(T), 269 | {keep_state, State#state{heartbeat_timer = Tmr}}; 270 | opened(_EvtType, {close, Reason}, State = #state{config = Config}) -> 271 | %% We send the first close frame and wait for the reply. 272 | %% TODO: stop all sessions writing 273 | %% We could still accept incoming frames (See: 2.4.6) 274 | ok = notify_closed(Config, Reason), 275 | case send_close(State, Reason) of 276 | ok -> {next_state, close_sent, State}; 277 | {error, closed} -> {stop, normal, State}; 278 | Error -> {stop, Error, State} 279 | end; 280 | opened(_EvtType, #'v1_0.close'{error = Error}, State = #state{config = Config}) -> 281 | %% We receive the first close frame, reply and terminate. 282 | ok = notify_closed(Config, translate_err(Error)), 283 | _ = send_close(State, none), 284 | {stop, normal, State}; 285 | opened({call, From}, begin_session, State) -> 286 | {Ret, State1} = handle_begin_session(From, State), 287 | {keep_state, State1, [{reply, From, Ret}]}; 288 | opened(info, {'DOWN', MRef, _, _, _Info}, 289 | State = #state{reader_m_ref = MRef, config = Config}) -> 290 | %% reader has gone down and we are not already shutting down 291 | ok = notify_closed(Config, shutdown), 292 | {stop, normal, State}; 293 | opened(_EvtType, Frame, State) -> 294 | error_logger:warning_msg("Unexpected connection frame ~p when in state ~p ~n", 295 | [Frame, State]), 296 | {keep_state, State}. 297 | 298 | close_sent(_EvtType, heartbeat, State) -> 299 | {next_state, close_sent, State}; 300 | close_sent(_EvtType, #'v1_0.close'{}, State) -> 301 | %% TODO: we should probably set up a timer before this to ensure 302 | %% we close down event if no reply is received 303 | {stop, normal, State}. 304 | 305 | set_other_procs0(OtherProcs, State) -> 306 | #{sessions_sup := SessionsSup, 307 | reader := Reader} = OtherProcs, 308 | ReaderMRef = monitor(process, Reader), 309 | amqp10_client_frame_reader:set_connection(Reader, self()), 310 | State#state{sessions_sup = SessionsSup, 311 | reader_m_ref = ReaderMRef, 312 | reader = Reader}. 313 | 314 | terminate(Reason, _StateName, #state{connection_sup = Sup, 315 | config = Config}) -> 316 | ok = notify_closed(Config, Reason), 317 | case Reason of 318 | normal -> sys:terminate(Sup, normal); 319 | _ -> ok 320 | end, 321 | ok. 322 | 323 | code_change(_OldVsn, StateName, State, _Extra) -> 324 | {ok, StateName, State}. 325 | 326 | %% ------------------------------------------------------------------- 327 | %% Internal functions. 328 | %% ------------------------------------------------------------------- 329 | 330 | handle_begin_session({FromPid, _Ref}, 331 | #state{sessions_sup = Sup, reader = Reader, 332 | next_channel = Channel, 333 | config = Config} = State) -> 334 | Ret = supervisor:start_child(Sup, [FromPid, Channel, Reader, Config]), 335 | State1 = case Ret of 336 | {ok, _} -> State#state{next_channel = Channel + 1}; 337 | _ -> State 338 | end, 339 | {Ret, State1}. 340 | 341 | send_open(#state{socket = Socket, config = Config}) -> 342 | {ok, Product} = application:get_key(description), 343 | {ok, Version} = application:get_key(vsn), 344 | Platform = "Erlang/OTP " ++ erlang:system_info(otp_release), 345 | Props = {map, [{{symbol, <<"product">>}, 346 | {utf8, list_to_binary(Product)}}, 347 | {{symbol, <<"version">>}, 348 | {utf8, list_to_binary(Version)}}, 349 | {{symbol, <<"platform">>}, 350 | {utf8, list_to_binary(Platform)}} 351 | ]}, 352 | ContainerId = maps:get(container_id, Config, generate_container_id()), 353 | IdleTimeOut = maps:get(idle_time_out, Config, 0), 354 | Open0 = #'v1_0.open'{container_id = {utf8, ContainerId}, 355 | channel_max = {ushort, 100}, 356 | idle_time_out = {uint, IdleTimeOut}, 357 | properties = Props}, 358 | Open1 = case Config of 359 | #{max_frame_size := MFSz} -> 360 | Open0#'v1_0.open'{max_frame_size = {uint, MFSz}}; 361 | _ -> Open0 362 | end, 363 | Open = case Config of 364 | #{hostname := Hostname} -> 365 | Open1#'v1_0.open'{hostname = {utf8, Hostname}}; 366 | _ -> Open1 367 | end, 368 | Encoded = amqp10_framing:encode_bin(Open), 369 | Frame = amqp10_binary_generator:build_frame(0, Encoded), 370 | ?DBG("CONN <- ~p~n", [Open]), 371 | socket_send(Socket, Frame). 372 | 373 | 374 | send_close(#state{socket = Socket}, _Reason) -> 375 | Close = #'v1_0.close'{}, 376 | Encoded = amqp10_framing:encode_bin(Close), 377 | Frame = amqp10_binary_generator:build_frame(0, Encoded), 378 | ?DBG("CONN <- ~p~n", [Close]), 379 | Ret = socket_send(Socket, Frame), 380 | case Ret of 381 | ok -> _ = 382 | socket_shutdown(Socket, write), 383 | ok; 384 | _ -> ok 385 | end, 386 | Ret. 387 | 388 | send_sasl_init(State, anon) -> 389 | Frame = #'v1_0.sasl_init'{mechanism = {symbol, <<"ANONYMOUS">>}}, 390 | send(Frame, 1, State); 391 | send_sasl_init(State, {plain, User, Pass}) -> 392 | Response = <<0:8, User/binary, 0:8, Pass/binary>>, 393 | Frame = #'v1_0.sasl_init'{mechanism = {symbol, <<"PLAIN">>}, 394 | initial_response = {binary, Response}}, 395 | send(Frame, 1, State). 396 | 397 | send(Record, FrameType, #state{socket = Socket}) -> 398 | Encoded = amqp10_framing:encode_bin(Record), 399 | Frame = amqp10_binary_generator:build_frame(0, FrameType, Encoded), 400 | ?DBG("CONN <- ~p~n", [Record]), 401 | socket_send(Socket, Frame). 402 | 403 | send_heartbeat(#state{socket = Socket}) -> 404 | Frame = amqp10_binary_generator:build_heartbeat_frame(), 405 | socket_send(Socket, Frame). 406 | 407 | socket_send({tcp, Socket}, Data) -> 408 | gen_tcp:send(Socket, Data); 409 | socket_send({ssl, Socket}, Data) -> 410 | ssl:send(Socket, Data). 411 | 412 | socket_shutdown({tcp, Socket}, How) -> 413 | gen_tcp:shutdown(Socket, How); 414 | socket_shutdown({ssl, Socket}, How) -> 415 | ssl:shutdown(Socket, How). 416 | 417 | notify_opened(#{notify_when_opened := none}) -> 418 | ok; 419 | notify_opened(#{notify_when_opened := Pid}) when is_pid(Pid) -> 420 | Pid ! amqp10_event(opened), 421 | ok; 422 | notify_opened(#{notify := Pid}) when is_pid(Pid) -> 423 | Pid ! amqp10_event(opened), 424 | ok; 425 | notify_opened(_) -> 426 | ok. 427 | 428 | notify_closed(#{notify_when_closed := none}, _Reason) -> 429 | ok; 430 | notify_closed(#{notify := none}, _Reason) -> 431 | ok; 432 | notify_closed(#{notify_when_closed := Pid}, Reason) when is_pid(Pid) -> 433 | Pid ! amqp10_event({closed, Reason}), 434 | ok; 435 | notify_closed(#{notify := Pid}, Reason) when is_pid(Pid) -> 436 | Pid ! amqp10_event({closed, Reason}), 437 | ok. 438 | 439 | start_heartbeat_timer(Timeout) -> 440 | timer:apply_after(Timeout, ?MODULE, heartbeat, [self()]). 441 | 442 | unpack(V) -> amqp10_client_types:unpack(V). 443 | 444 | -spec generate_container_id() -> binary(). 445 | generate_container_id() -> 446 | Pre = list_to_binary(atom_to_list(node())), 447 | Id = bin_to_hex(crypto:strong_rand_bytes(8)), 448 | <
>/binary, Id/binary>>.
449 | 
450 | bin_to_hex(Bin) ->
451 |     <<<= 10 -> N -10 + $a;
452 |            true  -> N + $0 end>>
453 |       || <> <= Bin>>.
454 | 
455 | translate_err(undefined) ->
456 |     none;
457 | translate_err(#'v1_0.error'{condition = Cond, description = Desc}) ->
458 |     Err =
459 |         case Cond of
460 |             ?V_1_0_AMQP_ERROR_INTERNAL_ERROR -> internal_error;
461 |             ?V_1_0_AMQP_ERROR_NOT_FOUND -> not_found;
462 |             ?V_1_0_AMQP_ERROR_UNAUTHORIZED_ACCESS -> unauthorized_access;
463 |             ?V_1_0_AMQP_ERROR_DECODE_ERROR -> decode_error;
464 |             ?V_1_0_AMQP_ERROR_RESOURCE_LIMIT_EXCEEDED -> resource_limit_exceeded;
465 |             ?V_1_0_AMQP_ERROR_NOT_ALLOWED -> not_allowed;
466 |             ?V_1_0_AMQP_ERROR_INVALID_FIELD -> invalid_field;
467 |             ?V_1_0_AMQP_ERROR_NOT_IMPLEMENTED -> not_implemented;
468 |             ?V_1_0_AMQP_ERROR_RESOURCE_LOCKED -> resource_locked;
469 |             ?V_1_0_AMQP_ERROR_PRECONDITION_FAILED -> precondition_failed;
470 |             ?V_1_0_AMQP_ERROR_RESOURCE_DELETED -> resource_deleted;
471 |             ?V_1_0_AMQP_ERROR_ILLEGAL_STATE -> illegal_state;
472 |             ?V_1_0_AMQP_ERROR_FRAME_SIZE_TOO_SMALL -> frame_size_too_small;
473 |             ?V_1_0_CONNECTION_ERROR_CONNECTION_FORCED -> forced;
474 |             ?V_1_0_CONNECTION_ERROR_FRAMING_ERROR -> framing_error;
475 |             ?V_1_0_CONNECTION_ERROR_REDIRECT -> redirect;
476 |             _ -> Cond
477 |         end,
478 |     {Err, unpack(Desc)}.
479 | 
480 | amqp10_event(Evt) ->
481 |     {amqp10_event, {connection, self(), Evt}}.
482 | 
483 | sasl_to_bin({plain, _, _}) -> <<"PLAIN">>;
484 | sasl_to_bin(anon) -> <<"ANONYMOUS">>.
485 | 
486 | config_defaults() ->
487 |     #{sasl => none,
488 |       transfer_limit_margin => 0,
489 |       max_frame_size => ?MAX_MAX_FRAME_SIZE}.
490 | 


--------------------------------------------------------------------------------
/src/amqp10_msg.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 | -module(amqp10_msg).
  8 | 
  9 | -export([from_amqp_records/1,
 10 |          to_amqp_records/1,
 11 |          % "read" api
 12 |          delivery_id/1,
 13 |          delivery_tag/1,
 14 |          handle/1,
 15 |          settled/1,
 16 |          message_format/1,
 17 |          headers/1,
 18 |          header/2,
 19 |          delivery_annotations/1,
 20 |          message_annotations/1,
 21 |          properties/1,
 22 |          application_properties/1,
 23 |          body/1,
 24 |          body_bin/1,
 25 |          footer/1,
 26 |          % "write" api
 27 |          new/2,
 28 |          new/3,
 29 |          set_handle/2,
 30 |          set_settled/2,
 31 |          set_message_format/2,
 32 |          set_headers/2,
 33 |          set_properties/2,
 34 |          set_application_properties/2,
 35 |          set_delivery_annotations/2,
 36 |          set_message_annotations/2
 37 |         ]).
 38 | 
 39 | -include_lib("amqp10_common/include/amqp10_framing.hrl").
 40 | 
 41 | -type maybe(T) :: T | undefined.
 42 | 
 43 | -type delivery_tag() :: binary().
 44 | -type content_type() :: term(). % TODO: refine
 45 | -type content_encoding() :: term(). % TODO: refine
 46 | 
 47 | % annotations keys are restricted to be of type symbol or of type ulong
 48 | -type annotations_key() :: binary() | non_neg_integer().
 49 | 
 50 | -type header_key() :: durable | priority | ttl | first_acquirer |
 51 |                       delivery_count.
 52 | 
 53 | -type amqp10_header() :: #{durable => boolean(), % false
 54 |                            priority => byte(), % 4
 55 |                            ttl => maybe(non_neg_integer()),
 56 |                            first_acquirer => boolean(), % false
 57 |                            delivery_count => non_neg_integer()}. % 0
 58 | 
 59 | -type amqp10_properties() :: #{message_id => maybe(any()),
 60 |                                user_id => maybe(binary()),
 61 |                                to => maybe(any()),
 62 |                                subject => maybe(binary()),
 63 |                                reply_to => maybe(any()),
 64 |                                correlation_id => maybe(any()),
 65 |                                content_type => maybe(content_type()),
 66 |                                content_encoding => maybe(content_encoding()),
 67 |                                absolute_expiry_time => maybe(non_neg_integer()),
 68 |                                creation_time => maybe(non_neg_integer()),
 69 |                                group_id => maybe(binary()),
 70 |                                group_sequence => maybe(non_neg_integer()),
 71 |                                reply_to_group_id => maybe(binary())}.
 72 | 
 73 | -type amqp10_body() :: [#'v1_0.data'{}] |
 74 |                        [#'v1_0.amqp_sequence'{}] |
 75 |                        #'v1_0.amqp_value'{}.
 76 | 
 77 | 
 78 | 
 79 | -record(amqp10_msg,
 80 |         {transfer :: #'v1_0.transfer'{},
 81 |          header :: maybe(#'v1_0.header'{}),
 82 |          delivery_annotations :: maybe(#'v1_0.delivery_annotations'{}),
 83 |          message_annotations :: maybe(#'v1_0.message_annotations'{}),
 84 |          properties :: maybe(#'v1_0.properties'{}),
 85 |          application_properties :: maybe(#'v1_0.application_properties'{}),
 86 |          body :: amqp10_body() | unset,
 87 |          footer :: maybe(#'v1_0.footer'{})
 88 |          }).
 89 | 
 90 | -opaque amqp10_msg() :: #amqp10_msg{}.
 91 | 
 92 | -export_type([amqp10_msg/0,
 93 |               amqp10_header/0,
 94 |               amqp10_properties/0,
 95 |               amqp10_body/0,
 96 |               delivery_tag/0
 97 |              ]).
 98 | 
 99 | -define(record_to_tuplelist(Rec, Ref),
100 |         lists:zip(record_info(fields, Rec), tl(tuple_to_list(Ref)))).
101 | 
102 | 
103 | %% API functions
104 | 
105 | -spec from_amqp_records([amqp10_client_types:amqp10_msg_record()]) ->
106 |     amqp10_msg().
107 | from_amqp_records([#'v1_0.transfer'{} = Transfer | Records]) ->
108 |     lists:foldl(fun parse_from_amqp/2, #amqp10_msg{transfer = Transfer,
109 |                                                    body = unset}, Records).
110 | 
111 | -spec to_amqp_records(amqp10_msg()) -> [amqp10_client_types:amqp10_msg_record()].
112 | to_amqp_records(#amqp10_msg{transfer = T,
113 |                             header = H,
114 |                             delivery_annotations = DAs,
115 |                             message_annotations = MAs,
116 |                             properties = Ps,
117 |                             application_properties = APs,
118 |                             body = B,
119 |                             footer = F
120 |                             }) ->
121 |     L = lists:flatten([T, H, DAs, MAs, Ps, APs, B, F]),
122 |     lists:filter(fun has_value/1, L).
123 | 
124 | -spec delivery_tag(amqp10_msg()) -> delivery_tag().
125 | delivery_tag(#amqp10_msg{transfer = #'v1_0.transfer'{delivery_tag = Tag}}) ->
126 |     unpack(Tag).
127 | 
128 | -spec delivery_id(amqp10_msg()) -> non_neg_integer().
129 | delivery_id(#amqp10_msg{transfer = #'v1_0.transfer'{delivery_id = Id}}) ->
130 |     unpack(Id).
131 | 
132 | -spec handle(amqp10_msg()) -> non_neg_integer().
133 | handle(#amqp10_msg{transfer = #'v1_0.transfer'{handle = Handle}}) ->
134 |     unpack(Handle).
135 | 
136 | -spec settled(amqp10_msg()) -> boolean().
137 | settled(#amqp10_msg{transfer = #'v1_0.transfer'{settled = Settled}}) ->
138 |     Settled.
139 | 
140 | % First 3 octets are the format
141 | % the last 1 octet is the version
142 | % See 2.8.11 in the spec
143 | -spec message_format(amqp10_msg()) ->
144 |     maybe({non_neg_integer(), non_neg_integer()}).
145 | message_format(#amqp10_msg{transfer =
146 |                          #'v1_0.transfer'{message_format = undefined}}) ->
147 |     undefined;
148 | message_format(#amqp10_msg{transfer =
149 |                          #'v1_0.transfer'{message_format = {uint, MF}}}) ->
150 |     <> = <>,
151 |     {Format, Version}.
152 | 
153 | 
154 | -spec headers(amqp10_msg()) -> amqp10_header().
155 | headers(#amqp10_msg{header = undefined}) -> #{};
156 | headers(#amqp10_msg{header = #'v1_0.header'{durable = Durable,
157 |                                             priority = Priority,
158 |                                             ttl = Ttl,
159 |                                             first_acquirer = FA,
160 |                                             delivery_count = DC}}) ->
161 |     Fields = [{durable, header_value(durable, Durable)},
162 |               {priority, header_value(priority, Priority)},
163 |               {ttl, header_value(ttl, Ttl)},
164 |               {first_acquirer, header_value(first_acquirer, FA)},
165 |               {delivery_count, header_value(delivery_count, DC)}],
166 | 
167 |     lists:foldl(fun ({_Key, undefined}, Acc) -> Acc;
168 |                     ({Key, Value}, Acc) -> Acc#{Key => Value}
169 |                 end, #{}, Fields).
170 | 
171 | -spec header(header_key(), amqp10_msg()) -> term().
172 | header(durable = K, #amqp10_msg{header = #'v1_0.header'{durable = D}}) ->
173 |     header_value(K, D);
174 | header(priority = K,
175 |        #amqp10_msg{header = #'v1_0.header'{priority = D}}) ->
176 |     header_value(K, D);
177 | header(ttl = K, #amqp10_msg{header = #'v1_0.header'{ttl = D}}) ->
178 |     header_value(K, D);
179 | header(first_acquirer = K,
180 |        #amqp10_msg{header = #'v1_0.header'{first_acquirer = D}}) ->
181 |     header_value(K, D);
182 | header(delivery_count = K,
183 |        #amqp10_msg{header = #'v1_0.header'{delivery_count = D}}) ->
184 |     header_value(K, D);
185 | header(K, #amqp10_msg{header = undefined}) -> header_value(K, undefined).
186 | 
187 | -spec delivery_annotations(amqp10_msg()) -> #{annotations_key() => any()}.
188 | delivery_annotations(#amqp10_msg{delivery_annotations = undefined}) ->
189 |     #{};
190 | delivery_annotations(#amqp10_msg{delivery_annotations =
191 |                                #'v1_0.delivery_annotations'{content = DAs}}) ->
192 |     lists:foldl(fun({K, V}, Acc) -> Acc#{unpack(K) => unpack(V)} end,
193 |                 #{}, DAs).
194 | 
195 | -spec message_annotations(amqp10_msg()) -> #{annotations_key() => any()}.
196 | message_annotations(#amqp10_msg{message_annotations = undefined}) ->
197 |     #{};
198 | message_annotations(#amqp10_msg{message_annotations =
199 |                                #'v1_0.message_annotations'{content = MAs}}) ->
200 |     lists:foldl(fun({K, V}, Acc) -> Acc#{unpack(K) => unpack(V)} end,
201 |                 #{}, MAs).
202 | 
203 | -spec properties(amqp10_msg()) -> amqp10_properties().
204 | properties(#amqp10_msg{properties = undefined}) -> #{};
205 | properties(#amqp10_msg{properties = Props}) ->
206 |     Fields = ?record_to_tuplelist('v1_0.properties', Props),
207 |     lists:foldl(fun ({_Key, undefined}, Acc) -> Acc;
208 |                     ({Key, Value}, Acc) -> Acc#{Key => unpack(Value)}
209 |                 end, #{}, Fields).
210 | 
211 | % application property values can be simple types - no maps or lists
212 | -spec application_properties(amqp10_msg()) ->
213 |     #{binary() => binary() | integer() | string()}.
214 | application_properties(#amqp10_msg{application_properties = undefined}) ->
215 |     #{};
216 | application_properties(
217 |   #amqp10_msg{application_properties =
218 |             #'v1_0.application_properties'{content = MAs}}) ->
219 |     lists:foldl(fun({K, V}, Acc) -> Acc#{unpack(K) => unpack(V)} end,
220 |                 #{}, MAs).
221 | 
222 | -spec footer(amqp10_msg()) -> #{annotations_key() => any()}.
223 | footer(#amqp10_msg{footer = undefined}) -> #{};
224 | footer(#amqp10_msg{footer = #'v1_0.footer'{content = Footer}}) ->
225 |     lists:foldl(fun({K, V}, Acc) -> Acc#{unpack(K) => unpack(V)} end, #{},
226 |                 Footer).
227 | 
228 | -spec body(amqp10_msg()) ->
229 |     [binary()] | [#'v1_0.amqp_sequence'{}] | #'v1_0.amqp_value'{}.
230 | body(#amqp10_msg{body = [#'v1_0.data'{} | _] = Data}) ->
231 |     [Content || #'v1_0.data'{content = Content} <- Data];
232 | body(#amqp10_msg{body = Body}) -> Body.
233 | 
234 | %% @doc Returns the binary representation
235 | -spec body_bin(amqp10_msg()) -> binary().
236 | body_bin(#amqp10_msg{body = [#'v1_0.data'{content = Bin}]})
237 |   when is_binary(Bin) ->
238 |     Bin;
239 | body_bin(#amqp10_msg{body = Data}) when is_list(Data) ->
240 |     iolist_to_binary([amqp10_framing:encode_bin(D) || D <- Data]);
241 | body_bin(#amqp10_msg{body = #'v1_0.amqp_value'{} = Body}) ->
242 |     %% TODO: to avoid unnecessary decoding and re-encoding we could amend
243 |     %% the parse to provide the body in a lazy fashion, only decoding when
244 |     %% reading. For now we just re-encode it.
245 |     iolist_to_binary(amqp10_framing:encode_bin(Body)).
246 | 
247 | %% @doc Create a new amqp10 message using the specified delivery tag, body
248 | %% and settlement state. Settled=true means the message is considered settled
249 | %% as soon as sent and no disposition will be issued by the receiver.
250 | %% Settled=false will delay settlement until a disposition has been received.
251 | %% A disposition will be notified to the sender by a message of the
252 | %% following stucture:
253 | %% {amqp10_disposition, {accepted | rejected, DeliveryTag}}
254 | -spec new(delivery_tag(), amqp10_body() | binary(), boolean()) -> amqp10_msg().
255 | new(DeliveryTag, Body, Settled) when is_binary(Body) ->
256 |     #amqp10_msg{transfer = #'v1_0.transfer'{delivery_tag = {binary, DeliveryTag},
257 |                                             settled = Settled,
258 |                                             message_format = {uint, 0}},
259 |                 body = [#'v1_0.data'{content = Body}]};
260 | new(DeliveryTag, Body, Settled) -> % TODO: constrain to amqp types
261 |     #amqp10_msg{transfer = #'v1_0.transfer'{delivery_tag = {binary, DeliveryTag},
262 |                                             settled = Settled,
263 |                                             message_format = {uint, 0}},
264 |                 body = Body}.
265 | 
266 | %% @doc Create a new settled amqp10 message using the specified delivery tag
267 | %% and body.
268 | -spec new(delivery_tag(), amqp10_body() | binary()) -> amqp10_msg().
269 | new(DeliveryTag, Body) ->
270 |     new(DeliveryTag, Body, false).
271 | 
272 | 
273 | % First 3 octets are the format
274 | % the last 1 octet is the version
275 | % See 2.8.11 in the spec
276 | %% @doc Set the message format.
277 | -spec set_message_format({non_neg_integer(), non_neg_integer()},
278 |                          amqp10_msg()) -> amqp10_msg().
279 | set_message_format({Format, Version}, #amqp10_msg{transfer = T} = Msg) ->
280 |     <> = <>,
281 |     Msg#amqp10_msg{transfer = T#'v1_0.transfer'{message_format =
282 |                                                 {uint, MsgFormat}}}.
283 | 
284 | %% @doc Set the link handle used for the message transfer.
285 | -spec set_handle(non_neg_integer(), amqp10_msg()) -> amqp10_msg().
286 | set_handle(Handle, #amqp10_msg{transfer = T} = Msg) ->
287 |     Msg#amqp10_msg{transfer = T#'v1_0.transfer'{handle = {uint, Handle}}}.
288 | 
289 | %% @doc Set the settledment mode.
290 | %% Settled=true means the message is considered settled
291 | %% as soon as sent and no disposition will be issued by the receiver.
292 | %% Settled=false will delay settlement until a disposition has been received.
293 | %% A disposition will be notified to the sender by a message of the
294 | %% following stucture:
295 | %% {amqp10_disposition, {accepted | rejected, DeliveryTag}}
296 | -spec set_settled(boolean(), amqp10_msg()) -> amqp10_msg().
297 | set_settled(Settled, #amqp10_msg{transfer = T} = Msg) ->
298 |     Msg#amqp10_msg{transfer = T#'v1_0.transfer'{settled = Settled}}.
299 | 
300 | %% @doc Set amqp message headers.
301 | -spec set_headers(#{atom() => any()}, amqp10_msg()) -> amqp10_msg().
302 | set_headers(Headers, #amqp10_msg{header = undefined} = Msg) ->
303 |     set_headers(Headers, Msg#amqp10_msg{header = #'v1_0.header'{}});
304 | set_headers(Headers, #amqp10_msg{header = Current} = Msg) ->
305 |     H = maps:fold(fun(durable, V, Acc) ->
306 |                           Acc#'v1_0.header'{durable = V};
307 |                      (priority, V, Acc) ->
308 |                           Acc#'v1_0.header'{priority = {uint, V}};
309 |                      (first_acquirer, V, Acc) ->
310 |                           Acc#'v1_0.header'{first_acquirer = V};
311 |                      (ttl, V, Acc) ->
312 |                           Acc#'v1_0.header'{ttl = {uint, V}};
313 |                      (delivery_count, V, Acc) ->
314 |                           Acc#'v1_0.header'{delivery_count = {uint, V}}
315 |                   end, Current, Headers),
316 |     Msg#amqp10_msg{header = H}.
317 | 
318 | %% @doc Set amqp message properties.
319 | -spec set_properties(amqp10_properties(), amqp10_msg()) -> amqp10_msg().
320 | set_properties(Props, #amqp10_msg{properties = undefined} = Msg) ->
321 |     set_properties(Props, Msg#amqp10_msg{properties = #'v1_0.properties'{}});
322 | set_properties(Props, #amqp10_msg{properties = Current} = Msg) ->
323 |     % TODO many fields are `any` types and we need to try to type tag them
324 |     P = maps:fold(fun(message_id, V, Acc) when is_binary(V) ->
325 |                           % message_id can be any type but we restrict it here
326 |                           Acc#'v1_0.properties'{message_id = utf8(V)};
327 |                      (user_id, V, Acc) ->
328 |                           Acc#'v1_0.properties'{user_id = utf8(V)};
329 |                      (to, V, Acc) ->
330 |                           Acc#'v1_0.properties'{to = utf8(V)};
331 |                      (subject, V, Acc) ->
332 |                           Acc#'v1_0.properties'{subject = utf8(V)};
333 |                      (reply_to, V, Acc) ->
334 |                           Acc#'v1_0.properties'{reply_to = utf8(V)};
335 |                      (correlation_id, V, Acc) ->
336 |                           Acc#'v1_0.properties'{correlation_id = utf8(V)};
337 |                      (content_type, V, Acc) ->
338 |                           Acc#'v1_0.properties'{content_type = sym(V)};
339 |                      (content_encoding, V, Acc) ->
340 |                           Acc#'v1_0.properties'{content_encoding = sym(V)};
341 |                      (absolute_expiry_time, V, Acc) ->
342 |                           Acc#'v1_0.properties'{absolute_expiry_time = {timestamp, V}};
343 |                      (creation_time, V, Acc) ->
344 |                           Acc#'v1_0.properties'{creation_time = {timestamp, V}};
345 |                      (group_id, V, Acc) ->
346 |                           Acc#'v1_0.properties'{group_id = utf8(V)};
347 |                      (group_sequence, V, Acc) ->
348 |                           Acc#'v1_0.properties'{group_sequence = uint(V)};
349 |                      (reply_to_group_id, V, Acc) ->
350 |                           Acc#'v1_0.properties'{reply_to_group_id = utf8(V)}
351 |                   end, Current, Props),
352 |     Msg#amqp10_msg{properties = P}.
353 | 
354 | -spec set_application_properties(#{binary() | string() => binary() | integer() | string()},
355 |                                  amqp10_msg()) -> amqp10_msg().
356 | set_application_properties(Props,
357 |                            #amqp10_msg{application_properties = undefined} =
358 |                            Msg) ->
359 |     APs = #'v1_0.application_properties'{content = []},
360 |     set_application_properties(Props,
361 |                                Msg#amqp10_msg{application_properties = APs});
362 | set_application_properties(
363 |   Props0, #amqp10_msg{application_properties =
364 |                       #'v1_0.application_properties'{content = APs0}} = Msg) ->
365 |     Props = maps:fold(fun (K, V, S) ->
366 |                               S#{utf8(K) => wrap_ap_value(V)}
367 |                       end, maps:from_list(APs0), Props0),
368 |     APs = #'v1_0.application_properties'{content = maps:to_list(Props)},
369 |     Msg#amqp10_msg{application_properties = APs}.
370 | 
371 | -spec set_delivery_annotations(#{binary() => binary() | integer() | string()},
372 |                                  amqp10_msg()) -> amqp10_msg().
373 | set_delivery_annotations(Props,
374 |                          #amqp10_msg{delivery_annotations = undefined} =
375 |                          Msg) ->
376 |     Anns = #'v1_0.delivery_annotations'{content = []},
377 |     set_delivery_annotations(Props,
378 |                              Msg#amqp10_msg{delivery_annotations = Anns});
379 | set_delivery_annotations(
380 |   Props0, #amqp10_msg{delivery_annotations =
381 |                       #'v1_0.delivery_annotations'{content = Anns0}} = Msg) ->
382 |     Anns = maps:fold(fun (K, V, S) ->
383 |                              S#{sym(K) => wrap_ap_value(V)}
384 |                      end, maps:from_list(Anns0), Props0),
385 |     Anns1 = #'v1_0.delivery_annotations'{content = maps:to_list(Anns)},
386 |     Msg#amqp10_msg{delivery_annotations = Anns1}.
387 | 
388 | -spec set_message_annotations(#{binary() => binary() | integer() | string()},
389 |                                  amqp10_msg()) -> amqp10_msg().
390 | set_message_annotations(Props,
391 |                          #amqp10_msg{message_annotations = undefined} =
392 |                          Msg) ->
393 |     Anns = #'v1_0.message_annotations'{content = []},
394 |     set_message_annotations(Props,
395 |                              Msg#amqp10_msg{message_annotations = Anns});
396 | set_message_annotations(
397 |   Props0, #amqp10_msg{message_annotations =
398 |                       #'v1_0.message_annotations'{content = Anns0}} = Msg) ->
399 |     Anns = maps:fold(fun (K, V, S) ->
400 |                              S#{sym(K) => wrap_ap_value(V)}
401 |                      end, maps:from_list(Anns0), Props0),
402 |     Anns1 = #'v1_0.message_annotations'{content = maps:to_list(Anns)},
403 |     Msg#amqp10_msg{message_annotations = Anns1}.
404 | 
405 | wrap_ap_value(true) ->
406 |     {boolean, true};
407 | wrap_ap_value(false) ->
408 |     {boolean, false};
409 | wrap_ap_value(V) when is_integer(V) ->
410 |     {uint, V};
411 | wrap_ap_value(V) when is_binary(V) ->
412 |     utf8(V);
413 | wrap_ap_value(V) when is_list(V) ->
414 |     utf8(list_to_binary(V));
415 | wrap_ap_value(V) when is_atom(V) ->
416 |     utf8(atom_to_list(V)).
417 | 
418 | 
419 | %% LOCAL
420 | header_value(durable, undefined) -> false;
421 | header_value(priority, undefined) -> 4;
422 | header_value(first_acquirer, undefined) -> false;
423 | header_value(delivery_count, undefined) -> 0;
424 | header_value(Key, {_Type, Value}) -> header_value(Key, Value);
425 | header_value(_Key, Value) -> Value.
426 | 
427 | parse_from_amqp(#'v1_0.header'{} = Header, AmqpMsg) ->
428 |     AmqpMsg#amqp10_msg{header = Header};
429 | parse_from_amqp(#'v1_0.delivery_annotations'{} = DAS, AmqpMsg) ->
430 |     AmqpMsg#amqp10_msg{delivery_annotations = DAS};
431 | parse_from_amqp(#'v1_0.message_annotations'{} = DAS, AmqpMsg) ->
432 |     AmqpMsg#amqp10_msg{message_annotations = DAS};
433 | parse_from_amqp(#'v1_0.properties'{} = Header, AmqpMsg) ->
434 |     AmqpMsg#amqp10_msg{properties = Header};
435 | parse_from_amqp(#'v1_0.application_properties'{} = APs, AmqpMsg) ->
436 |     AmqpMsg#amqp10_msg{application_properties = APs};
437 | parse_from_amqp(#'v1_0.amqp_value'{} = Value, AmqpMsg) ->
438 |     AmqpMsg#amqp10_msg{body = Value};
439 | parse_from_amqp(#'v1_0.amqp_sequence'{} = Seq, AmqpMsg) ->
440 |     AmqpMsg#amqp10_msg{body = [Seq]};
441 | parse_from_amqp(#'v1_0.data'{} = Data, AmqpMsg) ->
442 |     AmqpMsg#amqp10_msg{body = [Data]};
443 | parse_from_amqp(#'v1_0.footer'{} = Header, AmqpMsg) ->
444 |     AmqpMsg#amqp10_msg{footer = Header}.
445 | 
446 | unpack(V) -> amqp10_client_types:unpack(V).
447 | utf8(V) -> amqp10_client_types:utf8(V).
448 | sym(B) when is_list(B) -> {symbol, list_to_binary(B)};
449 | sym(B) when is_binary(B) -> {symbol, B}.
450 | uint(B) -> {uint, B}.
451 | 
452 | has_value(undefined) -> false;
453 | has_value(_) -> true.
454 | 


--------------------------------------------------------------------------------
/src/amqp10_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 | -module(amqp10_client).
  9 | 
 10 | -include("amqp10_client.hrl").
 11 | -include_lib("amqp10_common/include/amqp10_framing.hrl").
 12 | 
 13 | -export([open_connection/1,
 14 |          open_connection/2,
 15 |          close_connection/1,
 16 |          begin_session/1,
 17 |          begin_session_sync/1,
 18 |          begin_session_sync/2,
 19 |          end_session/1,
 20 |          attach_sender_link/3,
 21 |          attach_sender_link/4,
 22 |          attach_sender_link/5,
 23 |          attach_sender_link_sync/3,
 24 |          attach_sender_link_sync/4,
 25 |          attach_sender_link_sync/5,
 26 |          attach_receiver_link/3,
 27 |          attach_receiver_link/4,
 28 |          attach_receiver_link/5,
 29 |          attach_receiver_link/6,
 30 |          attach_receiver_link/7,
 31 |          attach_link/2,
 32 |          detach_link/1,
 33 |          send_msg/2,
 34 |          accept_msg/2,
 35 |          flow_link_credit/3,
 36 |          flow_link_credit/4,
 37 |          echo/1,
 38 |          link_handle/1,
 39 |          get_msg/1,
 40 |          get_msg/2,
 41 |          parse_uri/1
 42 |         ]).
 43 | 
 44 | -define(DEFAULT_TIMEOUT, 5000).
 45 | 
 46 | -type snd_settle_mode() :: amqp10_client_session:snd_settle_mode().
 47 | -type rcv_settle_mode() :: amqp10_client_session:rcv_settle_mode().
 48 | 
 49 | -type terminus_durability() :: amqp10_client_session:terminus_durability().
 50 | 
 51 | -type target_def() :: amqp10_client_session:target_def().
 52 | -type source_def() :: amqp10_client_session:source_def().
 53 | 
 54 | -type attach_role() :: amqp10_client_session:attach_role().
 55 | -type attach_args() :: amqp10_client_session:attach_args().
 56 | -type filter() :: amqp10_client_session:filter().
 57 | -type properties() :: amqp10_client_session:properties().
 58 | 
 59 | -type connection_config() :: amqp10_client_connection:connection_config().
 60 | 
 61 | -opaque link_ref() :: #link_ref{}.
 62 | 
 63 | -export_type([
 64 |               link_ref/0,
 65 |               snd_settle_mode/0,
 66 |               rcv_settle_mode/0,
 67 |               terminus_durability/0,
 68 |               target_def/0,
 69 |               source_def/0,
 70 |               attach_role/0,
 71 |               attach_args/0,
 72 |               connection_config/0
 73 |              ]).
 74 | 
 75 | -ifdef (OTP_RELEASE).
 76 |   -if(?OTP_RELEASE >= 23).
 77 |     -compile({nowarn_deprecated_function, [{http_uri, decode, 1}]}).
 78 |   -endif.
 79 | -endif.
 80 | 
 81 | %% @doc Convenience function for opening a connection providing only an
 82 | %% address and port. This uses anonymous sasl authentication.
 83 | %% This is asynchronous and will notify success/closure to the caller using
 84 | %% an amqp10_event of the following format:
 85 | %% {amqp10_event, {connection, ConnectionPid, opened | {closed, Why}}}
 86 | -spec open_connection(inet:socket_address() | inet:hostname(),
 87 |                       inet:port_number()) -> supervisor:startchild_ret().
 88 | open_connection(Addr, Port) ->
 89 |     open_connection(#{address => Addr, port => Port, notify => self(),
 90 |                       sasl => anon}).
 91 | 
 92 | %% @doc Opens a connection using a connection_config map
 93 | %% This is asynchronous and will notify success/closure to the caller using
 94 | %% an amqp10_event of the following format:
 95 | %% {amqp10_event, {connection, ConnectionPid, opened | {closed, Why}}}
 96 | -spec open_connection(connection_config()) ->
 97 |     supervisor:startchild_ret().
 98 | open_connection(ConnectionConfig0) ->
 99 |     Notify = maps:get(notify, ConnectionConfig0, self()),
100 |     NotifyWhenOpened = maps:get(notify_when_opened, ConnectionConfig0, self()),
101 |     NotifyWhenClosed = maps:get(notify_when_closed, ConnectionConfig0, self()),
102 |     amqp10_client_connection:open(ConnectionConfig0#{
103 |         notify => Notify,
104 |         notify_when_opened => NotifyWhenOpened,
105 |         notify_when_closed => NotifyWhenClosed
106 |     }).
107 | 
108 | %% @doc Opens a connection using a connection_config map
109 | %% This is asynchronous and will notify completion to the caller using
110 | %% an amqp10_event of the following format:
111 | %% {amqp10_event, {connection, ConnectionPid, {closed, Why}}}
112 | -spec close_connection(pid()) -> ok.
113 | close_connection(Pid) ->
114 |     amqp10_client_connection:close(Pid, none).
115 | 
116 | %% @doc Begins an amqp10 session using 'Connection'.
117 | %% This is asynchronous and will notify success/closure to the caller using
118 | %% an amqp10_event of the following format:
119 | %% {amqp10_event, {session, SessionPid, begun | {ended, Why}}}
120 | -spec begin_session(pid()) -> supervisor:startchild_ret().
121 | begin_session(Connection) when is_pid(Connection) ->
122 |     amqp10_client_connection:begin_session(Connection).
123 | 
124 | %% @doc Synchronously begins an amqp10 session using 'Connection'.
125 | %% This is a convenience function that awaits the 'begun' event
126 | %% for the newly created session before returning.
127 | -spec begin_session_sync(pid()) ->
128 |     supervisor:startchild_ret() | session_timeout.
129 | begin_session_sync(Connection) when is_pid(Connection) ->
130 |     begin_session_sync(Connection, ?DEFAULT_TIMEOUT).
131 | 
132 | %% @doc Synchronously begins an amqp10 session using 'Connection'.
133 | %% This is a convenience function that awaits the 'begun' event
134 | %% for the newly created session before returning.
135 | -spec begin_session_sync(pid(), non_neg_integer()) ->
136 |     supervisor:startchild_ret() | session_timeout.
137 | begin_session_sync(Connection, Timeout) when is_pid(Connection) ->
138 |     case begin_session(Connection) of
139 |         {ok, Session} ->
140 |             receive
141 |                 {amqp10_event, {session, Session, begun}} ->
142 |                     {ok, Session};
143 |                 {amqp10_event, {session, Session, {ended, Err}}} ->
144 |                     {error, Err}
145 |             after Timeout -> session_timeout
146 |             end;
147 |         Ret -> Ret
148 |     end.
149 | 
150 | %% @doc End an amqp10 session.
151 | %% This is asynchronous and will notify completion of the end request to the
152 | %% caller using an amqp10_event of the following format:
153 | %% {amqp10_event, {session, SessionPid, {ended, Why}}}
154 | -spec end_session(pid()) -> ok.
155 | end_session(Pid) ->
156 |     amqp10_client_session:'end'(Pid).
157 | 
158 | %% @doc Synchronously attach a link on 'Session'.
159 | %% This is a convenience function that awaits attached event
160 | %% for the link before returning.
161 | attach_sender_link_sync(Session, Name, Target) ->
162 |     attach_sender_link_sync(Session, Name, Target, mixed).
163 | 
164 | %% @doc Synchronously attach a link on 'Session'.
165 | %% This is a convenience function that awaits attached event
166 | %% for the link before returning.
167 | -spec attach_sender_link_sync(pid(), binary(), binary(),
168 |                               snd_settle_mode()) ->
169 |     {ok, link_ref()} | link_timeout.
170 | attach_sender_link_sync(Session, Name, Target, SettleMode) ->
171 |     attach_sender_link_sync(Session, Name, Target, SettleMode, none).
172 | 
173 | %% @doc Synchronously attach a link on 'Session'.
174 | %% This is a convenience function that awaits attached event
175 | %% for the link before returning.
176 | -spec attach_sender_link_sync(pid(), binary(), binary(),
177 |                               snd_settle_mode(), terminus_durability()) ->
178 |     {ok, link_ref()} | link_timeout.
179 | attach_sender_link_sync(Session, Name, Target, SettleMode, Durability) ->
180 |     {ok, Ref} = attach_sender_link(Session, Name, Target, SettleMode,
181 |                                    Durability),
182 |     receive
183 |         {amqp10_event, {link, Ref, attached}} ->
184 |             {ok, Ref};
185 |         {amqp10_event, {link, Ref, {detached, Err}}} ->
186 |             {error, Err}
187 |     after ?DEFAULT_TIMEOUT -> link_timeout
188 |     end.
189 | 
190 | %% @doc Attaches a sender link to a target.
191 | %% This is asynchronous and will notify completion of the attach request to the
192 | %% caller using an amqp10_event of the following format:
193 | %% {amqp10_event, {link, LinkRef, attached | {detached, Why}}}
194 | -spec attach_sender_link(pid(), binary(), binary()) -> {ok, link_ref()}.
195 | attach_sender_link(Session, Name, Target) ->
196 |     % mixed should work with any type of msg
197 |     attach_sender_link(Session, Name, Target, mixed).
198 | 
199 | %% @doc Attaches a sender link to a target.
200 | %% This is asynchronous and will notify completion of the attach request to the
201 | %% caller using an amqp10_event of the following format:
202 | %% {amqp10_event, {link, LinkRef, attached | {detached, Why}}}
203 | -spec attach_sender_link(pid(), binary(), binary(),
204 |                          snd_settle_mode()) ->
205 |     {ok, link_ref()}.
206 | attach_sender_link(Session, Name, Target, SettleMode) ->
207 |     attach_sender_link(Session, Name, Target, SettleMode, none).
208 | 
209 | %% @doc Attaches a sender link to a target.
210 | %% This is asynchronous and will notify completion of the attach request to the
211 | %% caller using an amqp10_event of the following format:
212 | %% {amqp10_event, {link, LinkRef, attached | {detached, Why}}}
213 | -spec attach_sender_link(pid(), binary(), binary(),
214 |                          snd_settle_mode(), terminus_durability()) ->
215 |     {ok, link_ref()}.
216 | attach_sender_link(Session, Name, Target, SettleMode, Durability) ->
217 |     AttachArgs = #{name => Name,
218 |                    role => {sender, #{address => Target,
219 |                                       durable => Durability}},
220 |                    snd_settle_mode => SettleMode,
221 |                    rcv_settle_mode => first},
222 |     amqp10_client_session:attach(Session, AttachArgs).
223 | 
224 | %% @doc Attaches a receiver link to a source.
225 | %% This is asynchronous and will notify completion of the attach request to the
226 | %% caller using an amqp10_event of the following format:
227 | %% {amqp10_event, {link, LinkRef, attached | {detached, Why}}}
228 | -spec attach_receiver_link(pid(), binary(), binary()) ->
229 |     {ok, link_ref()}.
230 | attach_receiver_link(Session, Name, Source) ->
231 |     attach_receiver_link(Session, Name, Source, settled).
232 | 
233 | %% @doc Attaches a receiver link to a source.
234 | %% This is asynchronous and will notify completion of the attach request to the
235 | %% caller using an amqp10_event of the following format:
236 | %% {amqp10_event, {link, LinkRef, attached | {detached, Why}}}
237 | -spec attach_receiver_link(pid(), binary(), binary(),
238 |                            snd_settle_mode()) ->
239 |     {ok, link_ref()}.
240 | attach_receiver_link(Session, Name, Source, SettleMode) ->
241 |     attach_receiver_link(Session, Name, Source, SettleMode, none).
242 | 
243 | %% @doc Attaches a receiver link to a source.
244 | %% This is asynchronous and will notify completion of the attach request to the
245 | %% caller using an amqp10_event of the following format:
246 | %% {amqp10_event, {link, LinkRef, attached | {detached, Why}}}
247 | -spec attach_receiver_link(pid(), binary(), binary(),
248 |                            snd_settle_mode(), terminus_durability()) ->
249 |     {ok, link_ref()}.
250 | attach_receiver_link(Session, Name, Source, SettleMode, Durability) ->
251 |     attach_receiver_link(Session, Name, Source, SettleMode, Durability, #{}).
252 | 
253 | %% @doc Attaches a receiver link to a source.
254 | %% This is asynchronous and will notify completion of the attach request to the
255 | %% caller using an amqp10_event of the following format:
256 | %% {amqp10_event, {link, LinkRef, attached | {detached, Why}}}
257 | -spec attach_receiver_link(pid(), binary(), binary(),
258 |                            snd_settle_mode(), terminus_durability(), filter()) ->
259 |     {ok, link_ref()}.
260 | attach_receiver_link(Session, Name, Source, SettleMode, Durability, Filter) ->
261 |     attach_receiver_link(Session, Name, Source, SettleMode, Durability, Filter, #{}).
262 | 
263 | %% @doc Attaches a receiver link to a source.
264 | %% This is asynchronous and will notify completion of the attach request to the
265 | %% caller using an amqp10_event of the following format:
266 | %% {amqp10_event, {link, LinkRef, attached | {detached, Why}}}
267 | -spec attach_receiver_link(pid(), binary(), binary(),
268 |                            snd_settle_mode(), terminus_durability(), filter(),
269 |                            properties()) ->
270 |     {ok, link_ref()}.
271 | attach_receiver_link(Session, Name, Source, SettleMode, Durability, Filter, Properties) ->
272 |     AttachArgs = #{name => Name,
273 |                    role => {receiver, #{address => Source,
274 |                                         durable => Durability}, self()},
275 |                    snd_settle_mode => SettleMode,
276 |                    rcv_settle_mode => first,
277 |                    filter => Filter,
278 |                    properties => Properties},
279 |     amqp10_client_session:attach(Session, AttachArgs).
280 | 
281 | -spec attach_link(pid(), attach_args()) -> {ok, link_ref()}.
282 | attach_link(Session, AttachArgs) ->
283 |     amqp10_client_session:attach(Session, AttachArgs).
284 | 
285 | %% @doc Detaches a link.
286 | %% This is asynchronous and will notify completion of the attach request to the
287 | %% caller using an amqp10_event of the following format:
288 | %% {amqp10_event, {link, LinkRef, {detached, Why}}}
289 | -spec detach_link(link_ref()) -> _.
290 | detach_link(#link_ref{link_handle = Handle, session = Session}) ->
291 |     amqp10_client_session:detach(Session, Handle).
292 | 
293 | %% @doc Grant credit to a sender.
294 | %% The amqp10_client will automatically grant more credit to the sender when
295 | %% the remaining link credit falls below the value of RenewWhenBelow.
296 | %% If RenewWhenBelow is 'never' the client will never grant new credit. Instead
297 | %% the caller will be notified when the link_credit reaches 0 with an
298 | %% amqp10_event of the following format:
299 | %% {amqp10_event, {link, LinkRef, credit_exhausted}}
300 | -spec flow_link_credit(link_ref(), Credit :: non_neg_integer(),
301 |                        RenewWhenBelow :: never | non_neg_integer()) -> ok.
302 | flow_link_credit(Ref, Credit, RenewWhenBelow) ->
303 |     flow_link_credit(Ref, Credit, RenewWhenBelow, false).
304 | 
305 | -spec flow_link_credit(link_ref(), Credit :: non_neg_integer(),
306 |                        RenewWhenBelow :: never | non_neg_integer(),
307 |                        Drain :: boolean()) -> ok.
308 | flow_link_credit(#link_ref{role = receiver, session = Session,
309 |                            link_handle = Handle},
310 |                  Credit, RenewWhenBelow, Drain) ->
311 |     Flow = #'v1_0.flow'{link_credit = {uint, Credit},
312 |                         drain = Drain},
313 |     ok = amqp10_client_session:flow(Session, Handle, Flow, RenewWhenBelow).
314 | 
315 | %% @doc Request that the sender's flow state is echoed back
316 | %% This may be used to determine when the Link has finally quiesced.
317 | %% see §2.6.10 of the spec
318 | echo(#link_ref{role = receiver, session = Session,
319 |                link_handle = Handle}) ->
320 |     Flow = #'v1_0.flow'{link_credit = {uint, 0},
321 |                         echo = true},
322 |     ok = amqp10_client_session:flow(Session, Handle, Flow, 0).
323 | 
324 | %%% messages
325 | 
326 | %% @doc Send a message on a the link referred to be the 'LinkRef'.
327 | %% Returns ok for "async" transfers when messages are sent with settled=true
328 | %% else it returns the delivery state from the disposition
329 | -spec send_msg(link_ref(), amqp10_msg:amqp10_msg()) ->
330 |     ok | {error, insufficient_credit | link_not_found | half_attached}.
331 | send_msg(#link_ref{role = sender, session = Session,
332 |                    link_handle = Handle}, Msg0) ->
333 |     Msg = amqp10_msg:set_handle(Handle, Msg0),
334 |     amqp10_client_session:transfer(Session, Msg, ?DEFAULT_TIMEOUT).
335 | 
336 | %% @doc Accept a message on a the link referred to be the 'LinkRef'.
337 | -spec accept_msg(link_ref(), amqp10_msg:amqp10_msg()) -> ok.
338 | accept_msg(#link_ref{role = receiver, session = Session}, Msg) ->
339 |     DeliveryId = amqp10_msg:delivery_id(Msg),
340 |     amqp10_client_session:disposition(Session, receiver, DeliveryId,
341 |                                       DeliveryId, true, accepted).
342 | 
343 | %% @doc Get a single message from a link.
344 | %% Flows a single link credit then awaits delivery or timeout.
345 | -spec get_msg(link_ref()) -> {ok, amqp10_msg:amqp10_msg()} | {error, timeout}.
346 | get_msg(LinkRef) ->
347 |     get_msg(LinkRef, ?DEFAULT_TIMEOUT).
348 | 
349 | %% @doc Get a single message from a link.
350 | %% Flows a single link credit then awaits delivery or timeout.
351 | -spec get_msg(link_ref(), non_neg_integer()) ->
352 |     {ok, amqp10_msg:amqp10_msg()} | {error, timeout}.
353 | get_msg(#link_ref{role = receiver} = Ref, Timeout) ->
354 |     %flow 1
355 |     ok = flow_link_credit(Ref, 1, never),
356 |     % wait for transfer
357 |     receive
358 |         {amqp10_msg, Ref, Message} -> {ok, Message}
359 |     after Timeout ->
360 |               {error, timeout}
361 |     end.
362 | 
363 | %% @doc Get the link handle from a LinkRef
364 | -spec link_handle(link_ref()) -> non_neg_integer().
365 | link_handle(#link_ref{link_handle = Handle}) -> Handle.
366 | 
367 | 
368 | -spec parse_uri(string()) ->
369 |     {ok, connection_config()} | {error, term()}.
370 | parse_uri(Uri) ->
371 |     case uri_string:parse(Uri) of
372 |         Map when is_map(Map) ->
373 |             try
374 |                 {ok, parse_result(Map)}
375 |             catch
376 |                 throw:Err -> {error, Err}
377 |             end;
378 |         {error, _, _} = Err -> Err
379 |     end.
380 | 
381 | parse_result(Map) ->
382 |     _ = case maps:get(path, Map, "/") of
383 |       "/" -> ok;
384 |       ""  -> ok;
385 |       _   -> throw(path_segment_not_supported)
386 |     end,
387 |     Scheme   = maps:get(scheme, Map, "amqp"),
388 |     UserInfo = maps:get(userinfo, Map, undefined),
389 |     Host     = maps:get(host, Map),
390 |     DefaultPort = case Scheme of
391 |       "amqp"  -> 5672;
392 |       "amqps" -> 5671
393 |     end,
394 |     Port   = maps:get(port, Map, DefaultPort),
395 |     Query0 = maps:get(query, Map, ""),
396 |     Query  = maps:from_list(uri_string:dissect_query(Query0)),
397 |     Sasl = case Query of
398 |                #{"sasl" := "anon"} -> anon;
399 |                #{"sasl" := "plain"} when UserInfo =:= undefined orelse length(UserInfo) =:= 0 ->
400 |                    throw(plain_sasl_missing_userinfo);
401 |                _ ->
402 |                    case UserInfo of
403 |                        [] -> none;
404 |                        undefined -> none;
405 |                        U -> parse_usertoken(U)
406 |                    end
407 |            end,
408 |     Ret0 = maps:fold(fun("idle_time_out", V, Acc) ->
409 |                              Acc#{idle_time_out => list_to_integer(V)};
410 |                         ("max_frame_size", V, Acc) ->
411 |                              Acc#{max_frame_size => list_to_integer(V)};
412 |                         ("hostname", V, Acc) ->
413 |                              Acc#{hostname => list_to_binary(V)};
414 |                         ("container_id", V, Acc) ->
415 |                              Acc#{container_id => list_to_binary(V)};
416 |                         ("transfer_limit_margin", V, Acc) ->
417 |                              Acc#{transfer_limit_margin => list_to_integer(V)};
418 |                         (_, _, Acc) -> Acc
419 |                      end, #{address => Host,
420 |                             hostname => to_binary(Host),
421 |                             port => Port,
422 |                             sasl => Sasl}, Query),
423 |     case Scheme of
424 |         "amqp"  -> Ret0;
425 |         "amqps" ->
426 |             TlsOpts = parse_tls_opts(Query),
427 |             Ret0#{tls_opts => {secure_port, TlsOpts}}
428 |     end.
429 | 
430 | 
431 | parse_usertoken(undefined) ->
432 |     none;
433 | parse_usertoken("") ->
434 |     none;
435 | parse_usertoken(U) ->
436 |     [User, Pass] = string:tokens(U, ":"),
437 |     {plain, to_binary(http_uri:decode(User)), to_binary(http_uri:decode(Pass))}.
438 | 
439 | parse_tls_opts(M) ->
440 |     lists:sort(maps:fold(fun parse_tls_opt/3, [], M)).
441 | 
442 | parse_tls_opt(K, V, Acc)
443 |   when K =:= "cacertfile" orelse
444 |        K =:= "certfile" orelse
445 |        K =:= "keyfile" ->
446 |     [{to_atom(K), V} | Acc];
447 | parse_tls_opt("verify", V, Acc)
448 |   when V =:= "verify_none" orelse
449 |        V =:= "verify_peer" ->
450 |     [{verify, to_atom(V)} | Acc];
451 | parse_tls_opt("verify", _V, _Acc) ->
452 |     throw({invalid_option, verify});
453 | parse_tls_opt("versions", V, Acc) ->
454 |     Parts = string:tokens(V, ","),
455 |     Versions = [try_to_existing_atom(P) || P <- Parts],
456 |     [{versions, Versions} | Acc];
457 | parse_tls_opt("fail_if_no_peer_cert", V, Acc)
458 |   when V =:= "true" orelse
459 |        V =:= "false" ->
460 |     [{fail_if_no_peer_cert, to_atom(V)} | Acc];
461 | parse_tls_opt("fail_if_no_peer_cert", _V, _Acc) ->
462 |     throw({invalid_option, fail_if_no_peer_cert});
463 | parse_tls_opt("server_name_indication", "disable", Acc) ->
464 |     [{server_name_indication, disable} | Acc];
465 | parse_tls_opt("server_name_indication", V, Acc) ->
466 |     [{server_name_indication, V} | Acc];
467 | parse_tls_opt(_K, _V, Acc) ->
468 |     Acc.
469 | 
470 | to_atom(X) when is_list(X) -> list_to_atom(X).
471 | to_binary(X) when is_list(X) -> list_to_binary(X).
472 | 
473 | try_to_existing_atom(L) when is_list(L) ->
474 |     try list_to_existing_atom(L) of
475 |         A -> A
476 |     catch
477 |         _:badarg ->
478 |             throw({non_existent_atom, L})
479 |     end.
480 | 
481 | 
482 | -ifdef(TEST).
483 | -include_lib("eunit/include/eunit.hrl").
484 | 
485 | parse_uri_test_() ->
486 |     [?_assertEqual({ok, #{address => "my_host",
487 |                           port => 9876,
488 |                           hostname => <<"my_host">>,
489 |                           sasl => none}}, parse_uri("amqp://my_host:9876")),
490 |      %% port defaults
491 |      ?_assertMatch({ok, #{port := 5671}}, parse_uri("amqps://my_host")),
492 |      ?_assertMatch({ok, #{port := 5672}}, parse_uri("amqp://my_host")),
493 |      ?_assertEqual({ok, #{address => "my_proxy",
494 |                           port => 9876,
495 |                           hostname => <<"my_host">>,
496 |                           container_id => <<"my_container">>,
497 |                           idle_time_out => 60000,
498 |                           max_frame_size => 512,
499 |                           tls_opts => {secure_port, []},
500 |                           sasl => {plain, <<"fred">>, <<"passw">>}}},
501 |                    parse_uri("amqps://fred:passw@my_proxy:9876?sasl=plain&"
502 |                              "hostname=my_host&container_id=my_container&"
503 |                              "max_frame_size=512&idle_time_out=60000")),
504 |      %% ensure URI encoded usernames and passwords are decodeded
505 |      ?_assertEqual({ok, #{address => "my_proxy",
506 |                           port => 9876,
507 |                           hostname => <<"my_proxy">>,
508 |                           sasl => {plain, <<"fr/ed">>, <<"pa/ssw">>}}},
509 |                    parse_uri("amqp://fr%2Fed:pa%2Fssw@my_proxy:9876")),
510 |      %% make sasl plain implicit when username and password is present
511 |      ?_assertEqual({ok, #{address => "my_proxy",
512 |                           port => 9876,
513 |                           hostname => <<"my_proxy">>,
514 |                           sasl => {plain, <<"fred">>, <<"passw">>}}},
515 |                    parse_uri("amqp://fred:passw@my_proxy:9876")),
516 |      ?_assertEqual(
517 |         {ok, #{address => "my_proxy", port => 9876,
518 |                hostname => <<"my_proxy">>,
519 |                tls_opts => {secure_port, [{cacertfile, "/etc/cacertfile.pem"},
520 |                                           {certfile, "/etc/certfile.pem"},
521 |                                           {fail_if_no_peer_cert, true},
522 |                                           {keyfile, "/etc/keyfile.key"},
523 |                                           {server_name_indication, "frazzle"},
524 |                                           {verify, verify_none},
525 |                                           {versions, ['tlsv1.1', 'tlsv1.2']}
526 |                                          ]},
527 |                sasl => {plain, <<"fred">>, <<"passw">>}}},
528 |         parse_uri("amqps://fred:passw@my_proxy:9876?sasl=plain"
529 |                   "&cacertfile=/etc/cacertfile.pem&certfile=/etc/certfile.pem"
530 |                   "&keyfile=/etc/keyfile.key&verify=verify_none"
531 |                   "&fail_if_no_peer_cert=true"
532 |                   "&server_name_indication=frazzle"
533 |                   "&versions=tlsv1.1,tlsv1.2")),
534 |      %% invalid tls version
535 |      ?_assertEqual({error, {non_existent_atom, "tlsv1.9999999"}},
536 |                    parse_uri("amqps://fred:passw@my_proxy:9876?sasl=plain&" ++
537 |                   "versions=tlsv1.1,tlsv1.9999999")),
538 |      ?_assertEqual(
539 |         {ok, #{address => "my_proxy",
540 |                port => 9876,
541 |                hostname => <<"my_proxy">>,
542 |                tls_opts => {secure_port, [{server_name_indication, disable}]},
543 |                sasl => {plain, <<"fred">>, <<"passw">>}}},
544 |         parse_uri("amqps://fred:passw@my_proxy:9876?sasl=plain"
545 |                   "&server_name_indication=disable")),
546 |      ?_assertEqual({error, {invalid_option, verify}},
547 |                    parse_uri("amqps://fred:passw@my_proxy:9876?sasl=plain&" ++
548 |                   "cacertfile=/etc/cacertfile.pem&certfile=/etc/certfile.pem&" ++
549 |                   "keyfile=/etc/keyfile.key&verify=verify_bananas")),
550 |      ?_assertEqual({error, {invalid_option, fail_if_no_peer_cert}},
551 |                    parse_uri("amqps://fred:passw@my_proxy:9876?sasl=plain&" ++
552 |                   "cacertfile=/etc/cacertfile.pem&certfile=/etc/certfile.pem&" ++
553 |                   "keyfile=/etc/keyfile.key&fail_if_no_peer_cert=banana")),
554 |      ?_assertEqual({error, plain_sasl_missing_userinfo},
555 |                    parse_uri("amqp://my_host:9876?sasl=plain")),
556 |      ?_assertEqual({error, path_segment_not_supported},
557 |                    parse_uri("amqp://my_host/my_path_segment:9876"))
558 |     ].
559 | 
560 | -endif.
561 | 


--------------------------------------------------------------------------------