diff --git a/.gitignore b/.gitignore index 10c7e4b7a4b6..a0e7d87e0c53 100644 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,7 @@ !/deps/amqp10_common/ !/deps/oauth2_client/ !/deps/rabbitmq_amqp1_0/ +!/deps/rabbitmq_amqp_client/ !/deps/rabbitmq_auth_backend_cache/ !/deps/rabbitmq_auth_backend_http/ !/deps/rabbitmq_auth_backend_ldap/ diff --git a/deps/amqp10_client/BUILD.bazel b/deps/amqp10_client/BUILD.bazel index 6d865915a811..df8b879adae1 100644 --- a/deps/amqp10_client/BUILD.bazel +++ b/deps/amqp10_client/BUILD.bazel @@ -20,7 +20,7 @@ load( APP_NAME = "amqp10_client" -APP_DESCRIPTION = "AMQP 1.0 client from the RabbitMQ Project" +APP_DESCRIPTION = "AMQP 1.0 client" APP_MODULE = "amqp10_client_app" diff --git a/deps/amqp10_client/Makefile b/deps/amqp10_client/Makefile index 466bde568804..6e6629bd7a11 100644 --- a/deps/amqp10_client/Makefile +++ b/deps/amqp10_client/Makefile @@ -1,5 +1,5 @@ PROJECT = amqp10_client -PROJECT_DESCRIPTION = AMQP 1.0 client from the RabbitMQ Project +PROJECT_DESCRIPTION = AMQP 1.0 client PROJECT_MOD = amqp10_client_app define PROJECT_APP_EXTRA_KEYS diff --git a/deps/amqp10_client/src/amqp10_client.erl b/deps/amqp10_client/src/amqp10_client.erl index 32f91a5f7aea..bf00b531cc4c 100644 --- a/deps/amqp10_client/src/amqp10_client.erl +++ b/deps/amqp10_client/src/amqp10_client.erl @@ -42,8 +42,6 @@ parse_uri/1 ]). --define(DEFAULT_TIMEOUT, 5000). - -type snd_settle_mode() :: amqp10_client_session:snd_settle_mode(). -type rcv_settle_mode() :: amqp10_client_session:rcv_settle_mode(). @@ -134,7 +132,7 @@ begin_session(Connection) when is_pid(Connection) -> -spec begin_session_sync(pid()) -> supervisor:startchild_ret() | session_timeout. begin_session_sync(Connection) when is_pid(Connection) -> - begin_session_sync(Connection, ?DEFAULT_TIMEOUT). + begin_session_sync(Connection, ?TIMEOUT). %% @doc Synchronously begins an amqp10 session using 'Connection'. %% This is a convenience function that awaits the 'begun' event @@ -191,7 +189,7 @@ attach_sender_link_sync(Session, Name, Target, SettleMode, Durability) -> {ok, Ref}; {amqp10_event, {link, Ref, {detached, Err}}} -> {error, Err} - after ?DEFAULT_TIMEOUT -> link_timeout + after ?TIMEOUT -> link_timeout end. %% @doc Attaches a sender link to a target. @@ -357,7 +355,7 @@ stop_receiver_link(#link_ref{role = receiver, send_msg(#link_ref{role = sender, session = Session, link_handle = Handle}, Msg0) -> Msg = amqp10_msg:set_handle(Handle, Msg0), - amqp10_client_session:transfer(Session, Msg, ?DEFAULT_TIMEOUT). + amqp10_client_session:transfer(Session, Msg, ?TIMEOUT). %% @doc Accept a message on a the link referred to be the 'LinkRef'. -spec accept_msg(link_ref(), amqp10_msg:amqp10_msg()) -> ok. @@ -376,7 +374,7 @@ settle_msg(LinkRef, Msg, Settlement) -> %% Flows a single link credit then awaits delivery or timeout. -spec get_msg(link_ref()) -> {ok, amqp10_msg:amqp10_msg()} | {error, timeout}. get_msg(LinkRef) -> - get_msg(LinkRef, ?DEFAULT_TIMEOUT). + get_msg(LinkRef, ?TIMEOUT). %% @doc Get a single message from a link. %% Flows a single link credit then awaits delivery or timeout. diff --git a/deps/amqp10_client/src/amqp10_client_session.erl b/deps/amqp10_client/src/amqp10_client_session.erl index 7b1cba641d76..ffdcfd5837a6 100644 --- a/deps/amqp10_client/src/amqp10_client_session.erl +++ b/deps/amqp10_client/src/amqp10_client_session.erl @@ -52,7 +52,6 @@ diff/2]). -define(MAX_SESSION_WINDOW_SIZE, 65535). --define(DEFAULT_TIMEOUT, 5000). -define(UINT_OUTGOING_WINDOW, {uint, ?UINT_MAX}). -define(INITIAL_OUTGOING_DELIVERY_ID, ?UINT_MAX). %% "The next-outgoing-id MAY be initialized to an arbitrary value" [2.5.6] @@ -149,7 +148,7 @@ reader :: pid(), socket :: amqp10_client_connection:amqp10_socket() | undefined, links = #{} :: #{output_handle() => #link{}}, - link_index = #{} :: #{link_name() => output_handle()}, + link_index = #{} :: #{{link_role(), link_name()} => output_handle()}, link_handle_index = #{} :: #{input_handle() => output_handle()}, next_link_handle = 0 :: output_handle(), early_attach_requests :: [term()], @@ -172,7 +171,7 @@ -spec begin_sync(pid()) -> supervisor:startchild_ret(). begin_sync(Connection) -> - begin_sync(Connection, ?DEFAULT_TIMEOUT). + begin_sync(Connection, ?TIMEOUT). -spec begin_sync(pid(), non_neg_integer()) -> supervisor:startchild_ret() | session_timeout. @@ -302,24 +301,28 @@ mapped(cast, #'v1_0.end'{error = Err}, State) -> mapped(cast, #'v1_0.attach'{name = {utf8, Name}, initial_delivery_count = IDC, handle = {uint, InHandle}, + role = PeerRoleBool, max_message_size = MaybeMaxMessageSize}, #state{links = Links, link_index = LinkIndex, link_handle_index = LHI} = State0) -> - #{Name := OutHandle} = LinkIndex, + OurRoleBool = not PeerRoleBool, + OurRole = boolean_to_role(OurRoleBool), + LinkIndexKey = {OurRole, Name}, + #{LinkIndexKey := OutHandle} = LinkIndex, #{OutHandle := Link0} = Links, ok = notify_link_attached(Link0), {DeliveryCount, MaxMessageSize} = case Link0 of - #link{role = sender, + #link{role = sender = OurRole, delivery_count = DC} -> MSS = case MaybeMaxMessageSize of {ulong, S} when S > 0 -> S; _ -> undefined end, {DC, MSS}; - #link{role = receiver, + #link{role = receiver = OurRole, max_message_size = MSS} -> {unpack(IDC), MSS} end, @@ -327,8 +330,8 @@ mapped(cast, #'v1_0.attach'{name = {utf8, Name}, input_handle = InHandle, delivery_count = DeliveryCount, max_message_size = MaxMessageSize}, - State = State0#state{links = Links#{OutHandle => Link}, - link_index = maps:remove(Name, LinkIndex), + State = State0#state{links = Links#{OutHandle := Link}, + link_index = maps:remove(LinkIndexKey, LinkIndex), link_handle_index = LHI#{InHandle => OutHandle}}, {keep_state, State}; mapped(cast, #'v1_0.detach'{handle = {uint, InHandle}, @@ -648,8 +651,8 @@ build_frames(Channel, Trf, Payload, MaxPayloadSize, Acc) -> make_source(#{role := {sender, _}}) -> #'v1_0.source'{}; -make_source(#{role := {receiver, #{address := Address} = Target, _Pid}, filter := Filter}) -> - Durable = translate_terminus_durability(maps:get(durable, Target, none)), +make_source(#{role := {receiver, #{address := Address} = Source, _Pid}, filter := Filter}) -> + Durable = translate_terminus_durability(maps:get(durable, Source, none)), TranslatedFilter = translate_filters(Filter), #'v1_0.source'{address = {utf8, Address}, durable = {uint, Durable}, @@ -743,35 +746,34 @@ detach_with_error_cond(Link = #link{output_handle = OutHandle}, State, Cond) -> ok = send(Detach, State), Link#link{state = detach_sent}. -send_attach(Send, #{name := Name, role := Role} = Args, {FromPid, _}, - #state{next_link_handle = OutHandle0, links = Links, +send_attach(Send, #{name := Name, role := RoleTuple} = Args, {FromPid, _}, + #state{next_link_handle = OutHandle0, links = Links, link_index = LinkIndex} = State) -> Source = make_source(Args), Target = make_target(Args), Properties = amqp10_client_types:make_properties(Args), - {LinkTarget, RoleAsBool, InitialDeliveryCount, MaxMessageSize} = - case Role of + {LinkTarget, InitialDeliveryCount, MaxMessageSize} = + case RoleTuple of {receiver, _, Pid} -> - {{pid, Pid}, true, undefined, max_message_size(Args)}; + {{pid, Pid}, undefined, max_message_size(Args)}; {sender, #{address := TargetAddr}} -> - {TargetAddr, false, uint(?INITIAL_DELIVERY_COUNT), undefined} - end, - - {OutHandle, NextLinkHandle} = - case Args of - #{handle := Handle} -> - %% Client app provided link handle. - %% Really only meant for integration tests. - {Handle, OutHandle0}; - _ -> - {OutHandle0, OutHandle0 + 1} + {TargetAddr, uint(?INITIAL_DELIVERY_COUNT), undefined} end, + {OutHandle, NextLinkHandle} = case Args of + #{handle := Handle} -> + %% Client app provided link handle. + %% Really only meant for integration tests. + {Handle, OutHandle0}; + _ -> + {OutHandle0, OutHandle0 + 1} + end, + Role = element(1, RoleTuple), % create attach performative Attach = #'v1_0.attach'{name = {utf8, Name}, - role = RoleAsBool, + role = role_to_boolean(Role), handle = {uint, OutHandle}, source = Source, properties = Properties, @@ -782,12 +784,12 @@ send_attach(Send, #{name := Name, role := Role} = Args, {FromPid, _}, max_message_size = MaxMessageSize}, ok = Send(Attach, State), - LinkRef = make_link_ref(element(1, Role), self(), OutHandle), + Ref = make_link_ref(Role, self(), OutHandle), Link = #link{name = Name, - ref = LinkRef, + ref = Ref, output_handle = OutHandle, state = attach_sent, - role = element(1, Role), + role = Role, notify = FromPid, auto_flow = never, target = LinkTarget, @@ -796,7 +798,7 @@ send_attach(Send, #{name := Name, role := Role} = Args, {FromPid, _}, {State#state{links = Links#{OutHandle => Link}, next_link_handle = NextLinkHandle, - link_index = LinkIndex#{Name => OutHandle}}, LinkRef}. + link_index = LinkIndex#{{Role, Name} => OutHandle}}, Ref}. -spec handle_session_flow(#'v1_0.flow'{}, #state{}) -> #state{}. handle_session_flow(#'v1_0.flow'{next_incoming_id = MaybeNII, @@ -1090,6 +1092,16 @@ sym(B) when is_atom(B) -> {symbol, atom_to_binary(B, utf8)}. reason(undefined) -> normal; reason(Other) -> Other. +role_to_boolean(sender) -> + ?AMQP_ROLE_SENDER; +role_to_boolean(receiver) -> + ?AMQP_ROLE_RECEIVER. + +boolean_to_role(?AMQP_ROLE_SENDER) -> + sender; +boolean_to_role(?AMQP_ROLE_RECEIVER) -> + receiver. + format_status(Status = #{data := Data0}) -> #state{channel = Channel, remote_channel = RemoteChannel, diff --git a/deps/amqp10_client/src/amqp10_client_types.erl b/deps/amqp10_client/src/amqp10_client_types.erl index 2a9859bcdf6c..fed585ac97e2 100644 --- a/deps/amqp10_client/src/amqp10_client_types.erl +++ b/deps/amqp10_client/src/amqp10_client_types.erl @@ -64,7 +64,7 @@ link_event_detail()}. -type amqp10_event() :: {amqp10_event, amqp10_event_detail()}. --type properties() :: #{binary() => tuple()}. +-type properties() :: #{binary() => amqp10_binary_generator:amqp10_prim()}. -export_type([amqp10_performative/0, channel/0, source/0, target/0, amqp10_msg_record/0, @@ -73,7 +73,6 @@ properties/0]). -unpack(undefined) -> undefined; unpack({_, Value}) -> Value; unpack(Value) -> Value. diff --git a/deps/amqp10_client/src/amqp10_msg.erl b/deps/amqp10_client/src/amqp10_msg.erl index f356782f8ba7..9302b2cce6de 100644 --- a/deps/amqp10_client/src/amqp10_msg.erl +++ b/deps/amqp10_client/src/amqp10_msg.erl @@ -6,6 +6,8 @@ %% -module(amqp10_msg). +-include_lib("amqp10_common/include/amqp10_types.hrl"). + -export([from_amqp_records/1, to_amqp_records/1, % "read" api @@ -256,12 +258,12 @@ body_bin(#amqp10_msg{body = #'v1_0.amqp_value'{} = Body}) -> new(DeliveryTag, Body, Settled) when is_binary(Body) -> #amqp10_msg{transfer = #'v1_0.transfer'{delivery_tag = {binary, DeliveryTag}, settled = Settled, - message_format = {uint, 0}}, + message_format = {uint, ?MESSAGE_FORMAT}}, body = [#'v1_0.data'{content = Body}]}; new(DeliveryTag, Body, Settled) -> % TODO: constrain to amqp types #amqp10_msg{transfer = #'v1_0.transfer'{delivery_tag = {binary, DeliveryTag}, settled = Settled, - message_format = {uint, 0}}, + message_format = {uint, ?MESSAGE_FORMAT}}, body = Body}. %% @doc Create a new settled amqp10 message using the specified delivery tag @@ -322,8 +324,13 @@ set_properties(Props, #amqp10_msg{properties = undefined} = Msg) -> set_properties(Props, Msg#amqp10_msg{properties = #'v1_0.properties'{}}); set_properties(Props, #amqp10_msg{properties = Current} = Msg) -> % TODO many fields are `any` types and we need to try to type tag them - P = maps:fold(fun(message_id, V, Acc) when is_binary(V) -> - % message_id can be any type but we restrict it here + P = maps:fold(fun(message_id, {T, _V} = TypeVal, Acc) when T =:= ulong orelse + T =:= uuid orelse + T =:= binary orelse + T =:= uf8 -> + Acc#'v1_0.properties'{message_id = TypeVal}; + (message_id, V, Acc) when is_binary(V) -> + %% backward compat clause Acc#'v1_0.properties'{message_id = utf8(V)}; (user_id, V, Acc) when is_binary(V) -> Acc#'v1_0.properties'{user_id = {binary, V}}; diff --git a/deps/amqp10_common/include/amqp10_types.hrl b/deps/amqp10_common/include/amqp10_types.hrl index 550c2bc773f3..3068f6efb4f5 100644 --- a/deps/amqp10_common/include/amqp10_types.hrl +++ b/deps/amqp10_common/include/amqp10_types.hrl @@ -10,3 +10,10 @@ -type transfer_number() :: sequence_no(). % [2.8.10] -type sequence_no() :: uint(). + +% [2.8.1] +-define(AMQP_ROLE_SENDER, false). +-define(AMQP_ROLE_RECEIVER, true). + +% [3.2.16] +-define(MESSAGE_FORMAT, 0). diff --git a/deps/amqp10_common/src/amqp10_binary_generator.erl b/deps/amqp10_common/src/amqp10_binary_generator.erl index a55f2eb74045..dd324ba374cc 100644 --- a/deps/amqp10_common/src/amqp10_binary_generator.erl +++ b/deps/amqp10_common/src/amqp10_binary_generator.erl @@ -52,6 +52,7 @@ -export_type([ amqp10_ctor/0, amqp10_type/0, + amqp10_prim/0, amqp10_described/0 ]). diff --git a/deps/amqp10_common/src/amqp10_framing.erl b/deps/amqp10_common/src/amqp10_framing.erl index ffbddf7e525c..5154affec009 100644 --- a/deps/amqp10_common/src/amqp10_framing.erl +++ b/deps/amqp10_common/src/amqp10_framing.erl @@ -100,14 +100,16 @@ symbolify(FieldName) when is_atom(FieldName) -> %% A sequence comes as an arbitrary list of values; it's not a %% composite type. -decode({described, Descriptor, {list, Fields}}) -> +decode({described, Descriptor, {list, Fields} = Type}) -> case amqp10_framing0:record_for(Descriptor) of #'v1_0.amqp_sequence'{} -> #'v1_0.amqp_sequence'{content = [decode(F) || F <- Fields]}; + #'v1_0.amqp_value'{} -> + #'v1_0.amqp_value'{content = Type}; Else -> fill_from_list(Else, Fields) end; -decode({described, Descriptor, {map, Fields}}) -> +decode({described, Descriptor, {map, Fields} = Type}) -> case amqp10_framing0:record_for(Descriptor) of #'v1_0.application_properties'{} -> #'v1_0.application_properties'{content = decode_map(Fields)}; @@ -117,13 +119,15 @@ decode({described, Descriptor, {map, Fields}}) -> #'v1_0.message_annotations'{content = decode_map(Fields)}; #'v1_0.footer'{} -> #'v1_0.footer'{content = decode_map(Fields)}; + #'v1_0.amqp_value'{} -> + #'v1_0.amqp_value'{content = Type}; Else -> fill_from_map(Else, Fields) end; -decode({described, Descriptor, {binary, Field}}) -> +decode({described, Descriptor, {binary, Field} = Type}) -> case amqp10_framing0:record_for(Descriptor) of #'v1_0.amqp_value'{} -> - #'v1_0.amqp_value'{content = {binary, Field}}; + #'v1_0.amqp_value'{content = Type}; #'v1_0.data'{} -> #'v1_0.data'{content = Field} end; diff --git a/deps/rabbit/BUILD.bazel b/deps/rabbit/BUILD.bazel index be46052ea417..e7291c046f77 100644 --- a/deps/rabbit/BUILD.bazel +++ b/deps/rabbit/BUILD.bazel @@ -1252,7 +1252,7 @@ rabbitmq_integration_suite( ], shard_count = 3, runtime_deps = [ - "//deps/amqp10_client:erlang_app", + "//deps/rabbitmq_amqp_client:erlang_app", ], ) @@ -1279,7 +1279,7 @@ rabbitmq_integration_suite( ":test_event_recorder_beam", ], runtime_deps = [ - "//deps/amqp10_client:erlang_app", + "//deps/rabbitmq_amqp_client:erlang_app", ], ) diff --git a/deps/rabbit/Makefile b/deps/rabbit/Makefile index 7964a1b4488b..c72a2687a46c 100644 --- a/deps/rabbit/Makefile +++ b/deps/rabbit/Makefile @@ -136,7 +136,7 @@ LOCAL_DEPS = sasl os_mon inets compiler public_key crypto ssl syntax_tools xmerl BUILD_DEPS = rabbitmq_cli DEPS = ranch rabbit_common amqp10_common rabbitmq_prelaunch ra sysmon_handler stdout_formatter recon redbug observer_cli osiris syslog systemd seshat khepri khepri_mnesia_migration -TEST_DEPS = rabbitmq_ct_helpers rabbitmq_ct_client_helpers meck proper amqp_client amqp10_client rabbitmq_amqp1_0 +TEST_DEPS = rabbitmq_ct_helpers rabbitmq_ct_client_helpers meck proper amqp_client rabbitmq_amqp_client rabbitmq_amqp1_0 PLT_APPS += mnesia diff --git a/deps/rabbit/app.bzl b/deps/rabbit/app.bzl index 5f64d4c1739a..09330dbd64d0 100644 --- a/deps/rabbit/app.bzl +++ b/deps/rabbit/app.bzl @@ -47,6 +47,7 @@ def all_beam_files(name = "all_beam_files"): "src/rabbit_access_control.erl", "src/rabbit_alarm.erl", "src/rabbit_amqp1_0.erl", + "src/rabbit_amqp_management.erl", "src/rabbit_amqp_reader.erl", "src/rabbit_amqp_session.erl", "src/rabbit_amqp_session_sup.erl", @@ -315,6 +316,7 @@ def all_test_beam_files(name = "all_test_beam_files"): "src/rabbit_access_control.erl", "src/rabbit_alarm.erl", "src/rabbit_amqp1_0.erl", + "src/rabbit_amqp_management.erl", "src/rabbit_amqp_reader.erl", "src/rabbit_amqp_session.erl", "src/rabbit_amqp_session_sup.erl", @@ -598,6 +600,7 @@ def all_srcs(name = "all_srcs"): "src/rabbit_access_control.erl", "src/rabbit_alarm.erl", "src/rabbit_amqp1_0.erl", + "src/rabbit_amqp_management.erl", "src/rabbit_amqp_reader.erl", "src/rabbit_amqp_session.erl", "src/rabbit_amqp_session_sup.erl", diff --git a/deps/rabbit/include/rabbit_amqp.hrl b/deps/rabbit/include/rabbit_amqp.hrl index 282cad9d4e47..efd7dfc663eb 100644 --- a/deps/rabbit/include/rabbit_amqp.hrl +++ b/deps/rabbit/include/rabbit_amqp.hrl @@ -25,9 +25,6 @@ %% [2.8.19] -define(MIN_MAX_FRAME_1_0_SIZE, 512). --define(SEND_ROLE, false). --define(RECV_ROLE, true). - %% for rabbit_event user_authentication_success and user_authentication_failure -define(AUTH_EVENT_KEYS, [name, diff --git a/deps/rabbit/src/mc_amqpl.erl b/deps/rabbit/src/mc_amqpl.erl index 0df202d1c67e..16e2eb7f0cea 100644 --- a/deps/rabbit/src/mc_amqpl.erl +++ b/deps/rabbit/src/mc_amqpl.erl @@ -26,7 +26,8 @@ message/4, message/5, from_basic_message/1, - to_091/2 + to_091/2, + from_091/2 ]). -import(rabbit_misc, diff --git a/deps/rabbit/src/rabbit.erl b/deps/rabbit/src/rabbit.erl index fbf1b9444340..f5f68c5a32bf 100644 --- a/deps/rabbit/src/rabbit.erl +++ b/deps/rabbit/src/rabbit.erl @@ -1709,7 +1709,8 @@ persist_static_configuration() -> _ -> ?MAX_MSG_SIZE end, - ok = persistent_term:put(max_message_size, MaxMsgSize). + ok = persistent_term:put(max_message_size, MaxMsgSize), + ok = rabbit_amqp_management:persist_static_configuration(). persist_static_configuration(Params) -> App = ?MODULE, diff --git a/deps/rabbit/src/rabbit_access_control.erl b/deps/rabbit/src/rabbit_access_control.erl index 6c3da3cd8d9d..cfc8b591eb3f 100644 --- a/deps/rabbit/src/rabbit_access_control.erl +++ b/deps/rabbit/src/rabbit_access_control.erl @@ -189,7 +189,7 @@ check_resource_access(User = #user{username = Username, check_access( fun() -> Module:check_resource_access( auth_user(User, Impl), Resource, Permission, Context) end, - Module, "~s access to ~s refused for user '~s'", + Module, "~s access to ~ts refused for user '~ts'", [Permission, rabbit_misc:rs(Resource), Username]); (_, Else) -> Else end, ok, Modules). @@ -202,7 +202,7 @@ check_topic_access(User = #user{username = Username, check_access( fun() -> Module:check_topic_access( auth_user(User, Impl), Resource, Permission, Context) end, - Module, "~s access to topic '~s' in exchange ~s refused for user '~s'", + Module, "~s access to topic '~ts' in exchange ~ts refused for user '~ts'", [Permission, maps:get(routing_key, Context), rabbit_misc:rs(Resource), Username]); (_, Else) -> Else end, ok, Modules). diff --git a/deps/rabbit/src/rabbit_amqp_management.erl b/deps/rabbit/src/rabbit_amqp_management.erl new file mode 100644 index 000000000000..760ff6c41abb --- /dev/null +++ b/deps/rabbit/src/rabbit_amqp_management.erl @@ -0,0 +1,702 @@ +-module(rabbit_amqp_management). + +-include("rabbit_amqp.hrl"). +-include_lib("rabbit_common/include/rabbit.hrl"). + +-export([persist_static_configuration/0, + handle_request/4]). + +-define(DEAD_LETTER_EXCHANGE_KEY, <<"x-dead-letter-exchange">>). +-define(MP_BINDING_URI_PATH_SEGMENT, mp_binding_uri_path_segment). + +-type resource_name() :: rabbit_types:exchange_name() | rabbit_types:rabbit_amqqueue_name(). + +-spec handle_request(binary(), rabbit_types:vhost(), rabbit_types:user(), pid()) -> iolist(). +handle_request(Request, Vhost, User, ConnectionPid) -> + ReqSections = amqp10_framing:decode_bin(Request), + ?DEBUG("~s Inbound request:~n ~tp", + [?MODULE, [amqp10_framing:pprint(Section) || Section <- ReqSections]]), + + {#'v1_0.properties'{ + message_id = MessageId, + to = {utf8, HttpRequestTarget}, + subject = {utf8, HttpMethod}, + %% see Link Pair CS 01 ยง2.1 + %% https://docs.oasis-open.org/amqp/linkpair/v1.0/cs01/linkpair-v1.0-cs01.html#_Toc51331305 + reply_to = {utf8, <<"$me">>}}, + ReqBody + } = decode_req(ReqSections, {undefined, undefined}), + + {StatusCode, + RespBody} = try {PathSegments, QueryMap} = parse_uri(HttpRequestTarget), + handle_http_req(HttpMethod, + PathSegments, + QueryMap, + ReqBody, + Vhost, + User, + ConnectionPid) + catch throw:{?MODULE, StatusCode0, Explanation} -> + rabbit_log:warning("request ~ts ~ts failed: ~ts", + [HttpMethod, HttpRequestTarget, Explanation]), + {StatusCode0, {utf8, Explanation}} + end, + + RespProps = #'v1_0.properties'{ + subject = {utf8, StatusCode}, + %% "To associate a response with a request, the correlation-id value of the response + %% properties MUST be set to the message-id value of the request properties." + %% [HTTP over AMQP WD 06 ยง5.1] + correlation_id = MessageId}, + RespAppProps = #'v1_0.application_properties'{ + content = [ + {{utf8, <<"http:response">>}, {utf8, <<"1.1">>}} + ]}, + RespDataSect = #'v1_0.amqp_value'{content = RespBody}, + RespSections = [RespProps, RespAppProps, RespDataSect], + [amqp10_framing:encode_bin(Sect) || Sect <- RespSections]. + +handle_http_req(<<"GET">>, + [<<"queues">>, QNameBinQuoted], + _Query, + null, + Vhost, + _User, + _ConnPid) -> + QNameBin = uri_string:unquote(QNameBinQuoted), + QName = rabbit_misc:r(Vhost, queue, QNameBin), + case rabbit_amqqueue:with( + QName, + fun(Q) -> + {ok, NumMsgs, NumConsumers} = rabbit_amqqueue:stat(Q), + RespPayload = encode_queue(Q, NumMsgs, NumConsumers), + {ok, {<<"200">>, RespPayload}} + end) of + {ok, Result} -> + Result; + {error, not_found} -> + throw(<<"404">>, "~ts not found", [rabbit_misc:rs(QName)]); + {error, {absent, Q, Reason}} -> + absent(Q, Reason) + end; + +handle_http_req(HttpMethod = <<"PUT">>, + PathSegments = [<<"queues">>, QNameBinQuoted], + Query, + ReqPayload, + Vhost, + User = #user{username = Username}, + ConnPid) -> + #{durable := Durable, + auto_delete := AutoDelete, + exclusive := Exclusive, + arguments := QArgs0 + } = decode_queue(ReqPayload), + QNameBin = uri_string:unquote(QNameBinQuoted), + Owner = case Exclusive of + true -> ConnPid; + false -> none + end, + QArgs = rabbit_amqqueue:augment_declare_args( + Vhost, Durable, Exclusive, AutoDelete, QArgs0), + case QNameBin of + <<>> -> throw(<<"400">>, "declare queue with empty name not allowed", []); + _ -> ok + end, + ok = prohibit_cr_lf(QNameBin), + QName = rabbit_misc:r(Vhost, queue, QNameBin), + ok = prohibit_reserved_amq(QName), + ok = check_resource_access(QName, configure, User), + rabbit_core_metrics:queue_declared(QName), + + {Q1, NumMsgs, NumConsumers, StatusCode} = + case rabbit_amqqueue:with( + QName, + fun(Q) -> + try rabbit_amqqueue:assert_equivalence( + Q, Durable, AutoDelete, QArgs, Owner) of + ok -> + {ok, Msgs, Consumers} = rabbit_amqqueue:stat(Q), + {ok, {Q, Msgs, Consumers, <<"200">>}} + catch exit:#amqp_error{name = precondition_failed, + explanation = Expl} -> + throw(<<"409">>, Expl, []); + exit:#amqp_error{explanation = Expl} -> + throw(<<"400">>, Expl, []) + end + end) of + {ok, Result} -> + Result; + {error, not_found} -> + ok = check_vhost_queue_limit(QName), + ok = check_dead_letter_exchange(QName, QArgs, User), + case rabbit_amqqueue:declare( + QName, Durable, AutoDelete, QArgs, Owner, Username) of + {new, Q} -> + rabbit_core_metrics:queue_created(QName), + {Q, 0, 0, <<"201">>}; + {owner_died, Q} -> + %% Presumably our own days are numbered since the + %% connection has died. Pretend the queue exists though, + %% just so nothing fails. + {Q, 0, 0, <<"201">>}; + {absent, Q, Reason} -> + absent(Q, Reason); + {existing, _Q} -> + %% Must have been created in the meantime. Loop around again. + handle_http_req(HttpMethod, PathSegments, Query, + ReqPayload, Vhost, User, ConnPid); + {protocol_error, _ErrorType, Reason, ReasonArgs} -> + throw(<<"400">>, Reason, ReasonArgs) + end; + {error, {absent, Q, Reason}} -> + absent(Q, Reason) + end, + + RespPayload = encode_queue(Q1, NumMsgs, NumConsumers), + {StatusCode, RespPayload}; + +handle_http_req(<<"PUT">>, + [<<"exchanges">>, XNameBinQuoted], + _Query, + ReqPayload, + Vhost, + User = #user{username = Username}, + _ConnPid) -> + XNameBin = uri_string:unquote(XNameBinQuoted), + #{type := XTypeBin, + durable := Durable, + auto_delete := AutoDelete, + internal := Internal, + arguments := XArgs + } = decode_exchange(ReqPayload), + XTypeAtom = try rabbit_exchange:check_type(XTypeBin) + catch exit:#amqp_error{explanation = Explanation} -> + throw(<<"400">>, Explanation, []) + end, + XName = rabbit_misc:r(Vhost, exchange, XNameBin), + ok = prohibit_default_exchange(XName), + ok = check_resource_access(XName, configure, User), + X = case rabbit_exchange:lookup(XName) of + {ok, FoundX} -> + FoundX; + {error, not_found} -> + ok = prohibit_cr_lf(XNameBin), + ok = prohibit_reserved_amq(XName), + rabbit_exchange:declare( + XName, XTypeAtom, Durable, AutoDelete, + Internal, XArgs, Username) + end, + try rabbit_exchange:assert_equivalence( + X, XTypeAtom, Durable, AutoDelete, Internal, XArgs) of + ok -> + {<<"204">>, null} + catch exit:#amqp_error{name = precondition_failed, + explanation = Expl} -> + throw(<<"409">>, Expl, []) + end; + +handle_http_req(<<"DELETE">>, + [<<"queues">>, QNameBinQuoted, <<"messages">>], + _Query, + null, + Vhost, + User, + ConnPid) -> + QNameBin = uri_string:unquote(QNameBinQuoted), + QName = rabbit_misc:r(Vhost, queue, QNameBin), + ok = check_resource_access(QName, read, User), + try rabbit_amqqueue:with_exclusive_access_or_die( + QName, ConnPid, + fun (Q) -> + case rabbit_queue_type:purge(Q) of + {ok, NumMsgs} -> + RespPayload = purge_or_delete_queue_response(NumMsgs), + {<<"200">>, RespPayload}; + {error, not_supported} -> + throw(<<"400">>, + "purge not supported by ~ts", + [rabbit_misc:rs(QName)]) + end + end) + catch exit:#amqp_error{explanation = Explanation} -> + throw(<<"400">>, Explanation, []) + end; + +handle_http_req(<<"DELETE">>, + [<<"queues">>, QNameBinQuoted], + _Query, + null, + Vhost, + User = #user{username = Username}, + ConnPid) -> + QNameBin = uri_string:unquote(QNameBinQuoted), + QName = rabbit_misc:r(Vhost, queue, QNameBin), + ok = prohibit_cr_lf(QNameBin), + ok = check_resource_access(QName, configure, User), + try rabbit_amqqueue:delete_with(QName, ConnPid, false, false, Username, true) of + {ok, NumMsgs} -> + RespPayload = purge_or_delete_queue_response(NumMsgs), + {<<"200">>, RespPayload} + catch exit:#amqp_error{explanation = Explanation} -> + throw(<<"400">>, Explanation, []) + end; + +handle_http_req(<<"DELETE">>, + [<<"exchanges">>, XNameBinQuoted], + _Query, + null, + Vhost, + User = #user{username = Username}, + _ConnPid) -> + XNameBin = uri_string:unquote(XNameBinQuoted), + XName = rabbit_misc:r(Vhost, exchange, XNameBin), + ok = prohibit_cr_lf(XNameBin), + ok = prohibit_default_exchange(XName), + ok = prohibit_reserved_amq(XName), + ok = check_resource_access(XName, configure, User), + _ = rabbit_exchange:delete(XName, false, Username), + {<<"204">>, null}; + +handle_http_req(<<"POST">>, + [<<"bindings">>], + _Query, + ReqPayload, + Vhost, + User = #user{username = Username}, + ConnPid) -> + #{source := SrcXNameBin, + binding_key := BindingKey, + arguments := Args} = BindingMap = decode_binding(ReqPayload), + {DstKind, DstNameBin} = case BindingMap of + #{destination_queue := Bin} -> + {queue, Bin}; + #{destination_exchange := Bin} -> + {exchange, Bin} + end, + SrcXName = rabbit_misc:r(Vhost, exchange, SrcXNameBin), + DstName = rabbit_misc:r(Vhost, DstKind, DstNameBin), + ok = binding_checks(SrcXName, DstName, BindingKey, User), + Binding = #binding{source = SrcXName, + destination = DstName, + key = BindingKey, + args = Args}, + ok = binding_action(add, Binding, Username, ConnPid), + {<<"204">>, null}; + +handle_http_req(<<"DELETE">>, + [<<"bindings">>, BindingSegment], + _Query, + null, + Vhost, + User = #user{username = Username}, + ConnPid) -> + {SrcXNameBin, + DstKind, + DstNameBin, + BindingKey, + ArgsHash} = decode_binding_path_segment(BindingSegment), + SrcXName = rabbit_misc:r(Vhost, exchange, SrcXNameBin), + DstName = rabbit_misc:r(Vhost, DstKind, DstNameBin), + ok = binding_checks(SrcXName, DstName, BindingKey, User), + Bindings = rabbit_binding:list_for_source_and_destination(SrcXName, DstName), + case search_binding(BindingKey, ArgsHash, Bindings) of + {value, Binding} -> + ok = binding_action(remove, Binding, Username, ConnPid); + false -> + ok + end, + {<<"204">>, null}; + +handle_http_req(<<"GET">>, + [<<"bindings">>], + QueryMap = #{<<"src">> := SrcXNameBin, + <<"key">> := Key}, + null, + Vhost, + _User, + _ConnPid) -> + {DstKind, + DstNameBin} = case QueryMap of + #{<<"dste">> := DstX} -> + {exchange, DstX}; + #{<<"dstq">> := DstQ} -> + {queue, DstQ}; + _ -> + throw(<<"400">>, + "missing 'dste' or 'dstq' in query: ~tp", + QueryMap) + end, + SrcXName = rabbit_misc:r(Vhost, exchange, SrcXNameBin), + DstName = rabbit_misc:r(Vhost, DstKind, DstNameBin), + Bindings0 = rabbit_binding:list_for_source_and_destination(SrcXName, DstName), + Bindings = [B || B = #binding{key = K} <- Bindings0, K =:= Key], + RespPayload = encode_bindings(Bindings), + {<<"200">>, RespPayload}. + +decode_queue({map, KVList}) -> + M = lists:foldl( + fun({{utf8, <<"durable">>}, V}, Acc) + when is_boolean(V) -> + Acc#{durable => V}; + ({{utf8, <<"exclusive">>}, V}, Acc) + when is_boolean(V) -> + Acc#{exclusive => V}; + ({{utf8, <<"auto_delete">>}, V}, Acc) + when is_boolean(V) -> + Acc#{auto_delete => V}; + ({{utf8, <<"arguments">>}, Args}, Acc) -> + Acc#{arguments => args_amqp_to_amqpl(Args)}; + (Prop, _Acc) -> + throw(<<"400">>, "bad queue property ~tp", [Prop]) + end, #{}, KVList), + Defaults = #{durable => true, + exclusive => false, + auto_delete => false, + arguments => []}, + maps:merge(Defaults, M). + +encode_queue(Q, NumMsgs, NumConsumers) -> + #resource{name = QNameBin} = amqqueue:get_name(Q), + Vhost = amqqueue:get_vhost(Q), + Durable = amqqueue:is_durable(Q), + AutoDelete = amqqueue:is_auto_delete(Q), + Exclusive = amqqueue:is_exclusive(Q), + QType = amqqueue:get_type(Q), + QArgs091 = amqqueue:get_arguments(Q), + QArgs = args_amqpl_to_amqp(QArgs091), + {Leader, Replicas} = queue_topology(Q), + KVList0 = [ + {{utf8, <<"message_count">>}, {ulong, NumMsgs}}, + {{utf8, <<"consumer_count">>}, {uint, NumConsumers}}, + {{utf8, <<"name">>}, {utf8, QNameBin}}, + {{utf8, <<"vhost">>}, {utf8, Vhost}}, + {{utf8, <<"durable">>}, {boolean, Durable}}, + {{utf8, <<"auto_delete">>}, {boolean, AutoDelete}}, + {{utf8, <<"exclusive">>}, {boolean, Exclusive}}, + {{utf8, <<"type">>}, {utf8, rabbit_queue_type:to_binary(QType)}}, + {{utf8, <<"arguments">>}, QArgs} + ], + KVList1 = if is_list(Replicas) -> + [{{utf8, <<"replicas">>}, + {array, utf8, [{utf8, atom_to_binary(R)} || R <- Replicas]} + } | KVList0]; + Replicas =:= undefined -> + KVList0 + end, + KVList = if is_atom(Leader) -> + [{{utf8, <<"leader">>}, + {utf8, atom_to_binary(Leader)} + } | KVList1]; + Leader =:= undefined -> + KVList1 + end, + {map, KVList}. + +%% The returned Replicas contain both online and offline replicas. +-spec queue_topology(amqqueue:amqqueue()) -> + {Leader :: undefined | node(), Replicas :: undefined | [node(),...]}. +queue_topology(Q) -> + case amqqueue:get_type(Q) of + rabbit_quorum_queue -> + [{leader, Leader0}, + {members, Members}] = rabbit_queue_type:info(Q, [leader, members]), + Leader = case Leader0 of + '' -> undefined; + _ -> Leader0 + end, + {Leader, Members}; + rabbit_stream_queue -> + #{name := StreamId} = amqqueue:get_type_state(Q), + case rabbit_stream_coordinator:members(StreamId) of + {ok, Members} -> + maps:fold(fun(Node, {_Pid, writer}, {_, Replicas}) -> + {Node, [Node | Replicas]}; + (Node, {_Pid, replica}, {Writer, Replicas}) -> + {Writer, [Node | Replicas]} + end, {undefined, []}, Members); + {error, _} -> + {undefined, undefined} + end; + _ -> + Pid = amqqueue:get_pid(Q), + Node = node(Pid), + {Node, [Node]} + end. + +decode_exchange({map, KVList}) -> + M = lists:foldl( + fun({{utf8, <<"durable">>}, V}, Acc) + when is_boolean(V) -> + Acc#{durable => V}; + ({{utf8, <<"auto_delete">>}, V}, Acc) + when is_boolean(V) -> + Acc#{auto_delete => V}; + ({{utf8, <<"type">>}, {utf8, V}}, Acc) -> + Acc#{type => V}; + ({{utf8, <<"internal">>}, V}, Acc) + when is_boolean(V) -> + Acc#{internal => V}; + ({{utf8, <<"arguments">>}, Args}, Acc) -> + Acc#{arguments => args_amqp_to_amqpl(Args)}; + (Prop, _Acc) -> + throw(<<"400">>, "bad exchange property ~tp", [Prop]) + end, #{}, KVList), + Defaults = #{durable => true, + auto_delete => false, + type => <<"direct">>, + internal => false, + arguments => []}, + maps:merge(Defaults, M). + +decode_binding({map, KVList}) -> + lists:foldl( + fun({{utf8, <<"source">>}, {utf8, V}}, Acc) -> + Acc#{source => V}; + ({{utf8, <<"destination_queue">>}, {utf8, V}}, Acc) -> + Acc#{destination_queue => V}; + ({{utf8, <<"destination_exchange">>}, {utf8, V}}, Acc) -> + Acc#{destination_exchange => V}; + ({{utf8, <<"binding_key">>}, {utf8, V}}, Acc) -> + Acc#{binding_key => V}; + ({{utf8, <<"arguments">>}, Args}, Acc) -> + Acc#{arguments => args_amqp_to_amqpl(Args)}; + (Field, _Acc) -> + throw(<<"400">>, "bad binding field ~tp", [Field]) + end, #{}, KVList). + +encode_bindings(Bindings) -> + Bs = lists:map( + fun(#binding{source = #resource{name = SrcName}, + key = BindingKey, + destination = #resource{kind = DstKind, + name = DstName}, + args = Args091}) -> + DstKindBin = case DstKind of + queue -> <<"queue">>; + exchange -> <<"exchange">> + end, + Args = args_amqpl_to_amqp(Args091), + Location = compose_binding_uri( + SrcName, DstKind, DstName, BindingKey, Args091), + KVList = [ + {{utf8, <<"source">>}, {utf8, SrcName}}, + {{utf8, <<"destination_", DstKindBin/binary>>}, {utf8, DstName}}, + {{utf8, <<"binding_key">>}, {utf8, BindingKey}}, + {{utf8, <<"arguments">>}, Args}, + {{utf8, <<"location">>}, {utf8, Location}} + ], + {map, KVList} + end, Bindings), + {list, Bs}. + +args_amqp_to_amqpl({map, KVList}) -> + lists:map(fun({{T, Key}, TypeVal}) + when T =:= utf8 orelse + T =:= symbol -> + mc_amqpl:to_091(Key, TypeVal); + (Arg) -> + throw(<<"400">>, + "unsupported argument ~tp", + [Arg]) + end, KVList). + +args_amqpl_to_amqp(Args) -> + {map, [{{utf8, K}, mc_amqpl:from_091(T, V)} || {K, T, V} <- Args]}. + +decode_req([], Acc) -> + Acc; +decode_req([#'v1_0.properties'{} = P | Rem], Acc) -> + decode_req(Rem, setelement(1, Acc, P)); +decode_req([#'v1_0.amqp_value'{content = C} | Rem], Acc) -> + decode_req(Rem, setelement(2, Acc, C)); +decode_req([_IgnoreSection | Rem], Acc) -> + decode_req(Rem, Acc). + +parse_uri(Uri) -> + case uri_string:normalize(Uri, [return_map]) of + UriMap = #{path := Path} -> + [<<>> | Segments] = binary:split(Path, <<"/">>, [global]), + QueryMap = case maps:find(query, UriMap) of + {ok, Query} -> + case uri_string:dissect_query(Query) of + QueryList + when is_list(QueryList) -> + maps:from_list(QueryList); + {error, Atom, Term} -> + throw(<<"400">>, + "failed to dissect query '~ts': ~s ~tp", + [Query, Atom, Term]) + end; + error -> + #{} + end, + {Segments, QueryMap}; + {error, Atom, Term} -> + throw(<<"400">>, + "failed to normalize URI '~ts': ~s ~tp", + [Uri, Atom, Term]) + end. + +compose_binding_uri(Src, DstKind, Dst, Key, Args) -> + SrcQ = uri_string:quote(Src), + DstQ = uri_string:quote(Dst), + KeyQ = uri_string:quote(Key), + ArgsHash = args_hash(Args), + DstChar = destination_kind_to_char(DstKind), + <<"/bindings/src=", SrcQ/binary, + ";dst", DstChar, $=, DstQ/binary, + ";key=", KeyQ/binary, + ";args=", ArgsHash/binary>>. + +-spec persist_static_configuration() -> ok. +persist_static_configuration() -> + %% This regex matches for example binding: + %% src=e1;dstq=q2;key=my-key;args= + %% Source, destination, and binding key values must be percent encoded. + %% Binding args use the URL safe Base 64 Alphabet: https://datatracker.ietf.org/doc/html/rfc4648#section-5 + {ok, MP} = re:compile( + <<"^src=([0-9A-Za-z\-.\_\~%]+);dst([eq])=([0-9A-Za-z\-.\_\~%]+);", + "key=([0-9A-Za-z\-.\_\~%]*);args=([0-9A-Za-z\-\_]*)$">>), + ok = persistent_term:put(?MP_BINDING_URI_PATH_SEGMENT, MP). + +decode_binding_path_segment(Segment) -> + MP = persistent_term:get(?MP_BINDING_URI_PATH_SEGMENT), + case re:run(Segment, MP, [{capture, all_but_first, binary}]) of + {match, [SrcQ, <>, DstQ, KeyQ, ArgsHash]} -> + Src = uri_string:unquote(SrcQ), + Dst = uri_string:unquote(DstQ), + Key = uri_string:unquote(KeyQ), + DstKind = destination_char_to_kind(DstKindChar), + {Src, DstKind, Dst, Key, ArgsHash}; + nomatch -> + throw(<<"400">>, "bad binding path segment '~s'", [Segment]) + end. + +destination_kind_to_char(exchange) -> $e; +destination_kind_to_char(queue) -> $q. + +destination_char_to_kind($e) -> exchange; +destination_char_to_kind($q) -> queue. + +search_binding(BindingKey, ArgsHash, Bindings) -> + lists:search(fun(#binding{key = Key, + args = Args}) + when Key =:= BindingKey -> + args_hash(Args) =:= ArgsHash; + (_) -> + false + end, Bindings). + +-spec args_hash(rabbit_framing:amqp_table()) -> binary(). +args_hash([]) -> + <<>>; +args_hash(Args) + when is_list(Args) -> + %% Args is already sorted. + Bin = <<(erlang:phash2(Args, 1 bsl 32)):32>>, + base64:encode(Bin, #{mode => urlsafe, + padding => false}). + +-spec binding_checks(rabbit_types:exchange_name(), + resource_name(), + rabbit_types:binding_key(), + rabbit_types:user()) -> ok. +binding_checks(SrcXName, DstName, BindingKey, User) -> + lists:foreach(fun(#resource{name = NameBin} = Name) -> + ok = prohibit_default_exchange(Name), + ok = prohibit_cr_lf(NameBin) + end, [SrcXName, DstName]), + ok = check_resource_access(DstName, write, User), + ok = check_resource_access(SrcXName, read, User), + case rabbit_exchange:lookup(SrcXName) of + {ok, SrcX} -> + rabbit_amqp_session:check_read_permitted_on_topic(SrcX, User, BindingKey); + {error, not_found} -> + ok + end. + +binding_action(Action, Binding, Username, ConnPid) -> + try rabbit_channel:binding_action(Action, Binding, Username, ConnPid) + catch exit:#amqp_error{explanation = Explanation} -> + throw(<<"400">>, Explanation, []) + end. + +purge_or_delete_queue_response(NumMsgs) -> + {map, [{{utf8, <<"message_count">>}, {ulong, NumMsgs}}]}. + +prohibit_cr_lf(NameBin) -> + case binary:match(NameBin, [<<"\n">>, <<"\r">>]) of + nomatch -> + ok; + _Found -> + throw(<<"400">>, + <<"Bad name '~ts': line feed and carriage return characters not allowed">>, + [NameBin]) + end. + +prohibit_default_exchange(#resource{kind = exchange, + name = <<"">>}) -> + throw(<<"403">>, <<"operation not permitted on the default exchange">>, []); +prohibit_default_exchange(_) -> + ok. + +-spec prohibit_reserved_amq(resource_name()) -> ok. +prohibit_reserved_amq(Res = #resource{name = <<"amq.", _/binary>>}) -> + throw(<<"403">>, + "~ts starts with reserved prefix 'amq.'", + [rabbit_misc:rs(Res)]); +prohibit_reserved_amq(#resource{}) -> + ok. + +-spec check_resource_access(resource_name(), + rabbit_types:permission_atom(), + rabbit_types:user()) -> ok. +check_resource_access(Resource, Perm, User) -> + try rabbit_access_control:check_resource_access(User, Resource, Perm, #{}) + catch exit:#amqp_error{name = access_refused, + explanation = Explanation} -> + %% For authorization failures, let's be more strict: Close the entire + %% AMQP session instead of only returning an HTTP Status Code 403. + rabbit_amqp_util:protocol_error( + ?V_1_0_AMQP_ERROR_UNAUTHORIZED_ACCESS, Explanation, []) + end. + +check_vhost_queue_limit(QName = #resource{virtual_host = Vhost}) -> + case rabbit_vhost_limit:is_over_queue_limit(Vhost) of + false -> + ok; + {true, Limit} -> + throw(<<"403">>, + "refused to declare ~ts because vhost queue limit ~b is reached", + [rabbit_misc:rs(QName), Limit]) + end. + + +check_dead_letter_exchange(QName = #resource{virtual_host = Vhost}, QArgs, User) -> + case rabbit_misc:r_arg(Vhost, exchange, QArgs, ?DEAD_LETTER_EXCHANGE_KEY) of + undefined -> + ok; + {error, {invalid_type, Type}} -> + throw(<<"400">>, + "invalid type '~ts' for arg '~s'", + [Type, ?DEAD_LETTER_EXCHANGE_KEY]); + DLX -> + ok = check_resource_access(QName, read, User), + ok = check_resource_access(DLX, write, User) + end. + +-spec absent(amqqueue:amqqueue(), + rabbit_amqqueue:absent_reason()) -> + no_return(). +absent(Queue, Reason) -> + {'EXIT', + #amqp_error{explanation = Explanation} + } = catch rabbit_amqqueue:absent(Queue, Reason), + throw(<<"400">>, Explanation, []). + +-spec throw(binary(), io:format(), [term()]) -> no_return(). +throw(StatusCode, Format, Data) -> + Reason0 = lists:flatten(io_lib:format(Format, Data)), + Reason = unicode:characters_to_binary(Reason0), + throw({?MODULE, StatusCode, Reason}). diff --git a/deps/rabbit/src/rabbit_amqp_reader.erl b/deps/rabbit/src/rabbit_amqp_reader.erl index fc3f8a4bc758..2d3190300eb2 100644 --- a/deps/rabbit/src/rabbit_amqp_reader.erl +++ b/deps/rabbit/src/rabbit_amqp_reader.erl @@ -127,9 +127,10 @@ system_code_change(Misc, _Module, _OldVsn, _Extra) -> {ok, Misc}. server_properties() -> - %% The atom doesn't match anything, it's just "not 0-9-1". - Raw = lists:keydelete(<<"capabilities">>, 1, rabbit_reader:server_properties(amqp_1_0)), - {map, [{{symbol, K}, {utf8, V}} || {K, longstr, V} <- Raw]}. + Props0 = rabbit_reader:server_properties(amqp_1_0), + Props1 = [{{symbol, K}, {utf8, V}} || {K, longstr, V} <- Props0], + Props = [{{symbol, <<"node">>}, {utf8, atom_to_binary(node())}} | Props1], + {map, Props}. %%-------------------------------------------------------------------------- @@ -491,6 +492,7 @@ handle_1_0_connection_frame( %% "the value in idle-time-out SHOULD be half the peer's actual timeout threshold" [2.4.5] idle_time_out = {uint, ReceiveTimeoutMillis div 2}, container_id = {utf8, rabbit_nodes:cluster_name()}, + offered_capabilities = {array, symbol, [{symbol, <<"LINK_PAIR_V1_0">>}]}, properties = server_properties()}), State; handle_1_0_connection_frame(#'v1_0.close'{}, State0) -> diff --git a/deps/rabbit/src/rabbit_amqp_session.erl b/deps/rabbit/src/rabbit_amqp_session.erl index 28fc6b321ef4..2610f3475d91 100644 --- a/deps/rabbit/src/rabbit_amqp_session.erl +++ b/deps/rabbit/src/rabbit_amqp_session.erl @@ -48,11 +48,15 @@ %% An even better approach in future would be to dynamically grow (or shrink) the link credit %% we grant depending on how fast target queue(s) actually confirm messages. -define(LINK_CREDIT_RCV, 128). +-define(MANAGEMENT_LINK_CREDIT_RCV, 8). +-define(MANAGEMENT_NODE_ADDRESS, <<"/management">>). -export([start_link/8, process_frame/2, list_local/0, - conserve_resources/3]). + conserve_resources/3, + check_read_permitted_on_topic/3 + ]). -export([init/1, terminate/2, @@ -75,6 +79,22 @@ settled :: boolean() }). +%% For AMQP management operations, we require a link pair as described in +%% https://docs.oasis-open.org/amqp/linkpair/v1.0/cs01/linkpair-v1.0-cs01.html +-record(management_link_pair, { + client_terminus_address, + incoming_half :: unattached | link_handle(), + outgoing_half :: unattached | link_handle() + }). + +%% Incoming or outgoing half of the link pair. +-record(management_link, { + name :: binary(), + delivery_count :: sequence_no(), + credit :: non_neg_integer(), + max_message_size :: unlimited | pos_integer() + }). + -record(incoming_link, { exchange :: rabbit_types:exchange() | rabbit_exchange:name(), routing_key :: undefined | rabbit_types:routing_key(), @@ -107,8 +127,7 @@ %% The queue sent us this consumer scoped sequence number. msg_id :: rabbit_amqqueue:msg_id(), consumer_tag :: rabbit_types:ctag(), - queue_name :: rabbit_amqqueue:name(), - delivered_at :: integer() + queue_name :: rabbit_amqqueue:name() }). -record(pending_transfer, { @@ -118,6 +137,10 @@ outgoing_unsettled :: #outgoing_unsettled{} }). +-record(pending_management_transfer, { + frames :: iolist() + }). + -record(cfg, { outgoing_max_frame_size :: unlimited | pos_integer(), reader_pid :: rabbit_types:connection(), @@ -190,7 +213,9 @@ %% Even when we are limited by session flow control, we must make sure to first send the TRANSFER to the %% client (once the remote_incoming_window got opened) followed by the FLOW with drain=true and credit=0 %% and advanced delivery count. Otherwise, we would violate the AMQP protocol spec. - outgoing_pending = queue:new() :: queue:queue(#pending_transfer{} | #'v1_0.flow'{}), + outgoing_pending = queue:new() :: queue:queue(#pending_transfer{} | + #pending_management_transfer{} | + #'v1_0.flow'{}), %% The link or session endpoint assigns each message a unique delivery-id %% from a session scoped sequence number. @@ -212,6 +237,10 @@ %% We send messages to clients on outgoing links. outgoing_links = #{} :: #{link_handle() => #outgoing_link{}}, + management_link_pairs = #{} :: #{LinkName :: binary() => #management_link_pair{}}, + incoming_management_links = #{} :: #{link_handle() => #management_link{}}, + outgoing_management_links = #{} :: #{link_handle() => #management_link{}}, + %% TRANSFER delivery IDs published to consuming clients but not yet acknowledged by clients. outgoing_unsettled_map = #{} :: #{delivery_number() => #outgoing_unsettled{}}, @@ -227,6 +256,8 @@ queue_states = rabbit_queue_type:init() :: rabbit_queue_type:state() }). +-type state() :: #state{}. + start_link(ReaderPid, WriterPid, ChannelNum, FrameMax, User, Vhost, ConnName, BeginFrame) -> Args = {ReaderPid, WriterPid, ChannelNum, FrameMax, User, Vhost, ConnName, BeginFrame}, Opts = [{hibernate_after, ?HIBERNATE_AFTER}], @@ -694,15 +725,129 @@ disposition(DeliveryState, First, Last) -> ?UINT(Last) end, #'v1_0.disposition'{ - role = ?RECV_ROLE, + role = ?AMQP_ROLE_RECEIVER, settled = true, state = DeliveryState, first = ?UINT(First), last = Last1}. -handle_control(#'v1_0.attach'{role = ?SEND_ROLE, +handle_control(#'v1_0.attach'{ + role = ?AMQP_ROLE_SENDER, + snd_settle_mode = ?V_1_0_SENDER_SETTLE_MODE_SETTLED, + name = Name = {utf8, LinkName}, + handle = Handle = ?UINT(HandleInt), + source = Source = #'v1_0.source'{address = ClientTerminusAddress}, + target = Target = #'v1_0.target'{address = {utf8, ?MANAGEMENT_NODE_ADDRESS}}, + initial_delivery_count = DeliveryCount = ?UINT(DeliveryCountInt), + properties = Properties + } = Attach, + #state{management_link_pairs = Pairs0, + incoming_management_links = Links + } = State0) -> + ok = validate_attach(Attach), + ok = check_paired(Properties), + Pairs = case Pairs0 of + #{LinkName := #management_link_pair{ + client_terminus_address = ClientTerminusAddress, + incoming_half = unattached, + outgoing_half = H} = Pair} + when is_integer(H) -> + maps:update(LinkName, + Pair#management_link_pair{incoming_half = HandleInt}, + Pairs0); + #{LinkName := Other} -> + protocol_error(?V_1_0_AMQP_ERROR_PRECONDITION_FAILED, + "received invalid attach ~p for management link pair ~p", + [Attach, Other]); + _ -> + maps:put(LinkName, + #management_link_pair{client_terminus_address = ClientTerminusAddress, + incoming_half = HandleInt, + outgoing_half = unattached}, + Pairs0) + end, + MaxMessageSize = persistent_term:get(max_message_size), + Link = #management_link{name = LinkName, + delivery_count = DeliveryCountInt, + credit = ?MANAGEMENT_LINK_CREDIT_RCV, + max_message_size = MaxMessageSize}, + State = State0#state{management_link_pairs = Pairs, + incoming_management_links = maps:put(HandleInt, Link, Links)}, + Reply = #'v1_0.attach'{ + name = Name, + handle = Handle, + %% We are the receiver. + role = ?AMQP_ROLE_RECEIVER, + snd_settle_mode = ?V_1_0_SENDER_SETTLE_MODE_SETTLED, + rcv_settle_mode = ?V_1_0_RECEIVER_SETTLE_MODE_FIRST, + source = Source, + target = Target, + max_message_size = {ulong, MaxMessageSize}, + properties = Properties}, + Flow = #'v1_0.flow'{handle = Handle, + delivery_count = DeliveryCount, + link_credit = ?UINT(?MANAGEMENT_LINK_CREDIT_RCV)}, + reply0([Reply, Flow], State); + +handle_control(#'v1_0.attach'{ + role = ?AMQP_ROLE_RECEIVER, + name = Name = {utf8, LinkName}, + handle = Handle = ?UINT(HandleInt), + source = Source = #'v1_0.source'{address = {utf8, ?MANAGEMENT_NODE_ADDRESS}}, + target = Target = #'v1_0.target'{address = ClientTerminusAddress}, + rcv_settle_mode = RcvSettleMode, + max_message_size = MaybeMaxMessageSize, + properties = Properties + } = Attach, + #state{management_link_pairs = Pairs0, + outgoing_management_links = Links + } = State0) -> + ok = validate_attach(Attach), + ok = check_paired(Properties), + Pairs = case Pairs0 of + #{LinkName := #management_link_pair{ + client_terminus_address = ClientTerminusAddress, + incoming_half = H, + outgoing_half = unattached} = Pair} + when is_integer(H) -> + maps:update(LinkName, + Pair#management_link_pair{outgoing_half = HandleInt}, + Pairs0); + #{LinkName := Other} -> + protocol_error(?V_1_0_AMQP_ERROR_PRECONDITION_FAILED, + "received invalid attach ~p for management link pair ~p", + [Attach, Other]); + _ -> + maps:put(LinkName, + #management_link_pair{client_terminus_address = ClientTerminusAddress, + incoming_half = unattached, + outgoing_half = HandleInt}, + Pairs0) + end, + MaxMessageSize = max_message_size(MaybeMaxMessageSize), + Link = #management_link{name = LinkName, + delivery_count = ?INITIAL_DELIVERY_COUNT, + credit = 0, + max_message_size = MaxMessageSize}, + State = State0#state{management_link_pairs = Pairs, + outgoing_management_links = maps:put(HandleInt, Link, Links)}, + Reply = #'v1_0.attach'{ + name = Name, + handle = Handle, + role = ?AMQP_ROLE_SENDER, + snd_settle_mode = ?V_1_0_SENDER_SETTLE_MODE_SETTLED, + rcv_settle_mode = RcvSettleMode, + source = Source, + target = Target, + initial_delivery_count = ?UINT(?INITIAL_DELIVERY_COUNT), + %% Echo back that we will respect the client's requested max-message-size. + max_message_size = MaybeMaxMessageSize, + properties = Properties}, + reply0(Reply, State); + +handle_control(#'v1_0.attach'{role = ?AMQP_ROLE_SENDER, name = LinkName, - handle = InputHandle = ?UINT(HandleInt), + handle = Handle = ?UINT(HandleInt), source = Source, snd_settle_mode = SndSettleMode, target = Target, @@ -721,21 +866,20 @@ handle_control(#'v1_0.attach'{role = ?SEND_ROLE, delivery_count = DeliveryCountInt, credit = ?LINK_CREDIT_RCV}, _Outcomes = outcomes(Source), - OutputHandle = output_handle(InputHandle), Reply = #'v1_0.attach'{ name = LinkName, - handle = OutputHandle, + handle = Handle, source = Source, snd_settle_mode = SndSettleMode, rcv_settle_mode = ?V_1_0_RECEIVER_SETTLE_MODE_FIRST, target = Target, %% We are the receiver. - role = ?RECV_ROLE, + role = ?AMQP_ROLE_RECEIVER, max_message_size = {ulong, persistent_term:get(max_message_size)}}, - Flow = #'v1_0.flow'{handle = OutputHandle, + Flow = #'v1_0.flow'{handle = Handle, delivery_count = DeliveryCount, link_credit = ?UINT(?LINK_CREDIT_RCV)}, - %%TODO check that handle is not present in either incoming_links or outgoing_links: + %%TODO check that handle is not in use for any other open links. %%"The handle MUST NOT be used for other open links. An attempt to attach %% using a handle which is already associated with a link MUST be responded to %% with an immediate close carrying a handle-in-use session-error." @@ -750,9 +894,9 @@ handle_control(#'v1_0.attach'{role = ?SEND_ROLE, [Reason]) end; -handle_control(#'v1_0.attach'{role = ?RECV_ROLE, +handle_control(#'v1_0.attach'{role = ?AMQP_ROLE_RECEIVER, name = LinkName, - handle = InputHandle = ?UINT(HandleInt), + handle = Handle = ?UINT(HandleInt), source = Source, snd_settle_mode = SndSettleMode, rcv_settle_mode = RcvSettleMode, @@ -820,10 +964,9 @@ handle_control(#'v1_0.attach'{role = ?RECV_ROLE, acting_user => Username}, case rabbit_queue_type:consume(Q, Spec, QStates0) of {ok, QStates} -> - OutputHandle = output_handle(InputHandle), A = #'v1_0.attach'{ name = LinkName, - handle = OutputHandle, + handle = Handle, initial_delivery_count = ?UINT(?INITIAL_DELIVERY_COUNT), snd_settle_mode = EffectiveSndSettleMode, rcv_settle_mode = RcvSettleMode, @@ -833,26 +976,15 @@ handle_control(#'v1_0.attach'{role = ?RECV_ROLE, source = Source#'v1_0.source'{ default_outcome = #'v1_0.released'{}, outcomes = outcomes(Source)}, - role = ?SEND_ROLE, + role = ?AMQP_ROLE_SENDER, %% Echo back that we will respect the client's requested max-message-size. max_message_size = MaybeMaxMessageSize}, - MaxMessageSize = case MaybeMaxMessageSize of - {ulong, Size} when Size > 0 -> - Size; - _ -> - %% "If this field is zero or unset, there is no - %% maximum size imposed by the link endpoint." - unlimited - end, + MaxMessageSize = max_message_size(MaybeMaxMessageSize), Link = #outgoing_link{queue_name_bin = QNameBin, queue_type = QType, send_settled = SndSettled, max_message_size = MaxMessageSize, delivery_count = DeliveryCount}, - %%TODO check that handle is not present in either incoming_links or outgoing_links: - %%"The handle MUST NOT be used for other open links. An attempt to attach - %% using a handle which is already associated with a link MUST be responded to - %% with an immediate close carrying a handle-in-use session-error." OutgoingLinks = OutgoingLinks0#{HandleInt => Link}, State1 = State0#state{queue_states = QStates, outgoing_links = OutgoingLinks}, @@ -881,26 +1013,25 @@ handle_control(#'v1_0.attach'{role = ?RECV_ROLE, handle_control({Txfr = #'v1_0.transfer'{handle = ?UINT(Handle)}, MsgPart}, State0 = #state{incoming_links = IncomingLinks}) -> + {Flows, State1} = session_flow_control_received_transfer(State0), + + {Reply, State} = case IncomingLinks of #{Handle := Link0} -> - {Flows, State1} = session_flow_control_received_transfer(State0), - case incoming_link_transfer(Txfr, MsgPart, Link0, State1) of + case incoming_link_transfer(Txfr, MsgPart, Link0, State1) of {ok, Reply0, Link, State2} -> - Reply = Reply0 ++ Flows, - State = State2#state{incoming_links = maps:update(Handle, Link, IncomingLinks)}, - reply0(Reply, State); + {Reply0, State2#state{incoming_links = IncomingLinks#{Handle := Link}}}; {error, Reply0} -> %% "When an error occurs at a link endpoint, the endpoint MUST be detached %% with appropriate error information supplied in the error field of the %% detach frame. The link endpoint MUST then be destroyed." [2.6.5] - Reply = Reply0 ++ Flows, - State = State1#state{incoming_links = maps:remove(Handle, IncomingLinks)}, - reply0(Reply, State) + {Reply0, State1#state{incoming_links = maps:remove(Handle, IncomingLinks)}} end; _ -> - protocol_error(?V_1_0_AMQP_ERROR_ILLEGAL_STATE, - "Unknown link handle: ~p", [Handle]) - end; + incoming_mgmt_link_transfer(Txfr, MsgPart, State1) + end, + reply0(Reply ++ Flows, State); + %% Although the AMQP message format [3.2] requires a body, it is valid to send a transfer frame without payload. %% For example, when a large multi transfer message is streamed using the ProtonJ2 client, the client could send @@ -913,37 +1044,43 @@ handle_control(Txfr = #'v1_0.transfer'{}, State) -> %% We'll deal with each of them separately. handle_control(#'v1_0.flow'{handle = Handle} = Flow, #state{incoming_links = IncomingLinks, - outgoing_links = OutgoingLinks} = State0) -> + outgoing_links = OutgoingLinks, + incoming_management_links = IncomingMgmtLinks, + outgoing_management_links = OutgoingMgmtLinks + } = State0) -> State = session_flow_control_received_flow(Flow, State0), - case Handle of - undefined -> - %% "If not set, the flow frame is carrying only information - %% pertaining to the session endpoint." [2.7.4] - {noreply, State}; - ?UINT(HandleInt) -> - %% "If set, indicates that the flow frame carries flow state information - %% for the local link endpoint associated with the given handle." [2.7.4] - case OutgoingLinks of - #{HandleInt := OutgoingLink} -> - {noreply, handle_outgoing_link_flow_control(OutgoingLink, Flow, State)}; - _ -> - case IncomingLinks of - #{HandleInt := _IncomingLink} -> - %% We're being told about available messages at - %% the sender. Yawn. TODO at least check transfer-count? - {noreply, State}; - _ -> - %% "If set to a handle that is not currently associated with - %% an attached link, the recipient MUST respond by ending the - %% session with an unattached-handle session error." [2.7.4] - rabbit_log:warning( - "Received Flow frame for unknown link handle: ~tp", [Flow]), - protocol_error( - ?V_1_0_SESSION_ERROR_UNATTACHED_HANDLE, - "Unattached link handle: ~b", [HandleInt]) - end - end - end; + S = case Handle of + undefined -> + %% "If not set, the flow frame is carrying only information + %% pertaining to the session endpoint." [2.7.4] + State; + ?UINT(HandleInt) -> + %% "If set, indicates that the flow frame carries flow state information + %% for the local link endpoint associated with the given handle." [2.7.4] + case OutgoingLinks of + #{HandleInt := OutgoingLink} -> + handle_outgoing_link_flow_control(OutgoingLink, Flow, State); + _ -> + case OutgoingMgmtLinks of + #{HandleInt := OutgoingMgmtLink} -> + handle_outgoing_mgmt_link_flow_control(OutgoingMgmtLink, Flow, State); + _ when is_map_key(HandleInt, IncomingLinks) orelse + is_map_key(HandleInt, IncomingMgmtLinks) -> + %% We're being told about available messages at the sender. + State; + _ -> + %% "If set to a handle that is not currently associated with + %% an attached link, the recipient MUST respond by ending the + %% session with an unattached-handle session error." [2.7.4] + rabbit_log:warning( + "Received Flow frame for unknown link handle: ~tp", [Flow]), + protocol_error( + ?V_1_0_SESSION_ERROR_UNATTACHED_HANDLE, + "Unattached link handle: ~b", [HandleInt]) + end + end + end, + {noreply, S}; handle_control(Detach = #'v1_0.detach'{handle = ?UINT(HandleInt)}, State0 = #state{queue_states = QStates0, @@ -1004,10 +1141,11 @@ handle_control(Detach = #'v1_0.detach'{handle = ?UINT(HandleInt)}, {Unsettled1, _RemovedMsgIds} = remove_link_from_outgoing_unsettled_map(Ctag, Unsettled0), {QStates0, Unsettled1, OutgoingLinks0} end, - State = State0#state{queue_states = QStates, - incoming_links = maps:remove(HandleInt, IncomingLinks), - outgoing_links = OutgoingLinks, - outgoing_unsettled_map = Unsettled}, + State1 = State0#state{queue_states = QStates, + incoming_links = maps:remove(HandleInt, IncomingLinks), + outgoing_links = OutgoingLinks, + outgoing_unsettled_map = Unsettled}, + State = maybe_detach_mgmt_link(HandleInt, State1), maybe_detach_reply(Detach, State, State0), publisher_or_consumer_deleted(State, State0), {noreply, State}; @@ -1026,7 +1164,7 @@ handle_control(#'v1_0.end'{}, end, {stop, normal, State}; -handle_control(#'v1_0.disposition'{role = ?RECV_ROLE, +handle_control(#'v1_0.disposition'{role = ?AMQP_ROLE_RECEIVER, first = ?UINT(First), last = Last0, state = Outcome, @@ -1093,7 +1231,7 @@ handle_control(#'v1_0.disposition'{role = ?RECV_ROLE, Reply = case DispositionSettled of true -> []; false -> [Disposition#'v1_0.disposition'{settled = true, - role = ?SEND_ROLE}] + role = ?AMQP_ROLE_SENDER}] end, State = handle_queue_actions(Actions, State1), reply0(Reply, State) @@ -1118,31 +1256,42 @@ send_pending(#state{remote_incoming_window = Space, {{value, #pending_transfer{frames = Frames} = Pending}, Buf1} when Space > 0 -> {NumTransfersSent, Buf, State1} = - case send_frames(WriterPid, Ch, Frames, Space) of - {all, SpaceLeft} -> - {Space - SpaceLeft, - Buf1, - record_outgoing_unsettled(Pending, State0)}; - {some, Rest} -> - {Space, - queue:in_r(Pending#pending_transfer{frames = Rest}, Buf1), - State0} - end, + case send_frames(WriterPid, Ch, Frames, Space) of + {sent_all, SpaceLeft} -> + {Space - SpaceLeft, + Buf1, + record_outgoing_unsettled(Pending, State0)}; + {sent_some, Rest} -> + {Space, + queue:in_r(Pending#pending_transfer{frames = Rest}, Buf1), + State0} + end, State2 = session_flow_control_sent_transfers(NumTransfersSent, State1), State = State2#state{outgoing_pending = Buf}, send_pending(State); - {{value, #pending_transfer{}}, _} - when Space =:= 0 -> + {{value, Pending = #pending_management_transfer{frames = Frames}}, Buf1} + when Space > 0 -> + {NumTransfersSent, Buf} = + case send_frames(WriterPid, Ch, Frames, Space) of + {sent_all, SpaceLeft} -> + {Space - SpaceLeft, Buf1}; + {sent_some, Rest} -> + {Space, queue:in_r(Pending#pending_management_transfer{frames = Rest}, Buf1)} + end, + State1 = session_flow_control_sent_transfers(NumTransfersSent, State0), + State = State1#state{outgoing_pending = Buf}, + send_pending(State); + _ when Space =:= 0 -> State0 end. -send_frames(_, _, [], Left) -> - {all, Left}; +send_frames(_, _, [], SpaceLeft) -> + {sent_all, SpaceLeft}; send_frames(_, _, Rest, 0) -> - {some, Rest}; -send_frames(Writer, Ch, [[Transfer, Sections] | Rest], Left) -> + {sent_some, Rest}; +send_frames(Writer, Ch, [[Transfer, Sections] | Rest], SpaceLeft) -> rabbit_amqp_writer:send_command(Writer, Ch, Transfer, Sections), - send_frames(Writer, Ch, Rest, Left - 1). + send_frames(Writer, Ch, Rest, SpaceLeft - 1). record_outgoing_unsettled(#pending_transfer{queue_ack_required = true, delivery_id = DeliveryId, @@ -1244,10 +1393,15 @@ settle_op_from_outcome(Outcome) -> "Unrecognised state: ~tp in DISPOSITION", [Outcome]). +-spec flow({uint, link_handle()}, sequence_no()) -> #'v1_0.flow'{}. flow(Handle, DeliveryCount) -> + flow(Handle, DeliveryCount, ?LINK_CREDIT_RCV). + +-spec flow({uint, link_handle()}, sequence_no(), non_neg_integer()) -> #'v1_0.flow'{}. +flow(Handle, DeliveryCount, LinkCredit) -> #'v1_0.flow'{handle = Handle, delivery_count = ?UINT(DeliveryCount), - link_credit = ?UINT(?LINK_CREDIT_RCV)}. + link_credit = ?UINT(LinkCredit)}. session_flow_fields(Frames, State) when is_list(Frames) -> @@ -1398,7 +1552,7 @@ handle_deliver(ConsumerTag, AckRequired, handle = ?UINT(Handle), delivery_id = ?UINT(DeliveryId), delivery_tag = {binary, Dtag}, - message_format = ?UINT(0), % [3.2.16] + message_format = ?UINT(?MESSAGE_FORMAT), settled = SendSettled}, Mc1 = mc:convert(mc_amqp, Mc0), Mc = mc:set_annotation(redelivered, Redelivered, Mc1), @@ -1408,14 +1562,7 @@ handle_deliver(ConsumerTag, AckRequired, [?MODULE, [amqp10_framing:pprint(Section) || Section <- amqp10_framing:decode_bin(iolist_to_binary(Sections))]]), validate_message_size(Sections, MaxMessageSize), - Frames = case MaxFrameSize of - unlimited -> - [[Transfer, Sections]]; - _ -> - %% TODO Ugh - TLen = iolist_size(amqp10_framing:encode_bin(Transfer)), - encode_frames(Transfer, Sections, MaxFrameSize - TLen, []) - end, + Frames = transfer_frames(Transfer, Sections, MaxFrameSize), messages_delivered(Redelivered, QType), rabbit_trace:tap_out(Msg, ConnName, ChannelNum, Username, Trace), OutgoingLinks = case DelCount of @@ -1428,11 +1575,7 @@ handle_deliver(ConsumerTag, AckRequired, Del = #outgoing_unsettled{ msg_id = MsgId, consumer_tag = ConsumerTag, - queue_name = QName, - %% The consumer timeout interval starts already from the point in time the - %% queue sent us the message so that the Ra log can be truncated even if - %% the message is sitting here for a long time. - delivered_at = os:system_time(millisecond)}, + queue_name = QName}, PendingTransfer = #pending_transfer{ frames = Frames, queue_ack_required = AckRequired, @@ -1479,6 +1622,103 @@ delivery_tag(MsgId = {Priority, Seq}, _) %%% Incoming Link %%% %%%%%%%%%%%%%%%%%%%%% +incoming_mgmt_link_transfer( + #'v1_0.transfer'{ + %% We only allow settled management requests + %% given that we are going to send a response anyway. + settled = true, + %% In the current implementation, we disallow large incoming management request messages. + more = false, + handle = IncomingHandle = ?UINT(IncomingHandleInt)}, + Request, + #state{management_link_pairs = LinkPairs, + incoming_management_links = IncomingLinks, + outgoing_management_links = OutgoingLinks, + outgoing_pending = Pending, + outgoing_delivery_id = OutgoingDeliveryId, + cfg = #cfg{outgoing_max_frame_size = MaxFrameSize, + vhost = Vhost, + user = User, + reader_pid = ReaderPid} + } = State0) -> + + IncomingLink0 = case maps:find(IncomingHandleInt, IncomingLinks) of + {ok, Link} -> + Link; + error -> + protocol_error( + ?V_1_0_AMQP_ERROR_ILLEGAL_STATE, + "Unknown link handle: ~p", [IncomingHandleInt]) + end, + #management_link{name = Name, + delivery_count = IncomingDeliveryCount0, + credit = IncomingCredit0, + max_message_size = IncomingMaxMessageSize + } = IncomingLink0, + case IncomingCredit0 > 0 of + true -> + ok; + false -> + protocol_error( + ?V_1_0_LINK_ERROR_TRANSFER_LIMIT_EXCEEDED, + "insufficient credit (~b) for management link from client to RabbitMQ", + [IncomingCredit0]) + end, + #management_link_pair{ + incoming_half = IncomingHandleInt, + outgoing_half = OutgoingHandleInt + } = maps:get(Name, LinkPairs), + OutgoingLink0 = case OutgoingHandleInt of + unattached -> + protocol_error( + ?V_1_0_AMQP_ERROR_PRECONDITION_FAILED, + "received transfer on half open management link pair", []); + _ -> + maps:get(OutgoingHandleInt, OutgoingLinks) + end, + #management_link{name = Name, + delivery_count = OutgoingDeliveryCount, + credit = OutgoingCredit, + max_message_size = OutgoingMaxMessageSize} = OutgoingLink0, + case OutgoingCredit > 0 of + true -> + ok; + false -> + protocol_error( + ?V_1_0_AMQP_ERROR_PRECONDITION_FAILED, + "insufficient credit (~b) for management link from RabbitMQ to client", + [OutgoingCredit]) + end, + validate_message_size(Request, IncomingMaxMessageSize), + Response = rabbit_amqp_management:handle_request(Request, Vhost, User, ReaderPid), + + Transfer = #'v1_0.transfer'{ + handle = ?UINT(OutgoingHandleInt), + delivery_id = ?UINT(OutgoingDeliveryId), + delivery_tag = {binary, <<>>}, + message_format = ?UINT(?MESSAGE_FORMAT), + settled = true}, + ?DEBUG("~s Outbound content:~n ~tp~n", + [?MODULE, [amqp10_framing:pprint(Section) || + Section <- amqp10_framing:decode_bin(iolist_to_binary(Response))]]), + validate_message_size(Response, OutgoingMaxMessageSize), + Frames = transfer_frames(Transfer, Response, MaxFrameSize), + PendingTransfer = #pending_management_transfer{frames = Frames}, + IncomingDeliveryCount = add(IncomingDeliveryCount0, 1), + IncomingCredit1 = IncomingCredit0 - 1, + {IncomingCredit, Reply} = maybe_grant_mgmt_link_credit( + IncomingCredit1, IncomingDeliveryCount, IncomingHandle), + IncomingLink = IncomingLink0#management_link{delivery_count = IncomingDeliveryCount, + credit = IncomingCredit}, + OutgoingLink = OutgoingLink0#management_link{delivery_count = add(OutgoingDeliveryCount, 1), + credit = OutgoingCredit - 1}, + State = State0#state{ + outgoing_delivery_id = add(OutgoingDeliveryId, 1), + outgoing_pending = queue:in(PendingTransfer, Pending), + incoming_management_links = maps:update(IncomingHandleInt, IncomingLink, IncomingLinks), + outgoing_management_links = maps:update(OutgoingHandleInt, OutgoingLink, OutgoingLinks)}, + {Reply, State}. + incoming_link_transfer( #'v1_0.transfer'{more = true, %% "The delivery-id MUST be supplied on the first transfer of a multi-transfer delivery." @@ -1555,6 +1795,7 @@ incoming_link_transfer( ok = validate_multi_transfer_settled(MaybeSettled, FirstSettled), {MsgBin0, FirstDeliveryId, FirstSettled} end, + validate_transfer_rcv_settle_mode(RcvSettleMode, Settled), validate_incoming_message_size(MsgBin), Sections = amqp10_framing:decode_bin(MsgBin), @@ -1571,12 +1812,6 @@ incoming_link_transfer( check_write_permitted_on_topic(X, User, RoutingKey), QNames = rabbit_exchange:route(X, Mc, #{return_binding_keys => true}), rabbit_trace:tap_in(Mc, QNames, ConnName, ChannelNum, Username, Trace), - case not Settled andalso - RcvSettleMode =:= ?V_1_0_RECEIVER_SETTLE_MODE_SECOND of - true -> protocol_error(?V_1_0_AMQP_ERROR_NOT_IMPLEMENTED, - "rcv-settle-mode second not supported", []); - false -> ok - end, Opts = #{correlation => {HandleInt, DeliveryId}}, Qs0 = rabbit_amqqueue:lookup_many(QNames), Qs = rabbit_amqqueue:prepend_extra_bcc(Qs0), @@ -1611,11 +1846,6 @@ incoming_link_transfer( {error, [Disposition, Detach]} end. -rabbit_exchange_lookup(X = #exchange{}) -> - {ok, X}; -rabbit_exchange_lookup(XName = #resource{}) -> - rabbit_exchange:lookup(XName). - ensure_routing_key(LinkRKey, Mc0) -> RKey = case LinkRKey of undefined -> @@ -1635,6 +1865,11 @@ ensure_routing_key(LinkRKey, Mc0) -> Mc = mc:set_annotation(?ANN_ROUTING_KEYS, [RKey], Mc0), {RKey, Mc}. +rabbit_exchange_lookup(X = #exchange{}) -> + {ok, X}; +rabbit_exchange_lookup(XName = #resource{}) -> + rabbit_exchange:lookup(XName). + process_routing_confirm([], _SenderSettles = true, _, U) -> rabbit_global_counters:messages_unroutable_dropped(?PROTOCOL, 1), {U, []}; @@ -1651,7 +1886,7 @@ process_routing_confirm([_|_] = Qs, SenderSettles, DeliveryId, U0) -> {U, []}. released(DeliveryId) -> - #'v1_0.disposition'{role = ?RECV_ROLE, + #'v1_0.disposition'{role = ?AMQP_ROLE_RECEIVER, first = ?UINT(DeliveryId), settled = true, state = #'v1_0.released'{}}. @@ -1682,6 +1917,13 @@ grant_link_credit(Credit, NumUnconfirmed) -> Credit =< ?LINK_CREDIT_RCV / 2 andalso NumUnconfirmed < ?LINK_CREDIT_RCV. +maybe_grant_mgmt_link_credit(Credit, DeliveryCount, Handle) + when Credit =< ?MANAGEMENT_LINK_CREDIT_RCV / 2 -> + {?MANAGEMENT_LINK_CREDIT_RCV, + [flow(Handle, DeliveryCount, ?MANAGEMENT_LINK_CREDIT_RCV)]}; +maybe_grant_mgmt_link_credit(Credit, _, _) -> + {Credit, []}. + %% TODO default-outcome and outcomes, dynamic lifetimes ensure_target(#'v1_0.target'{dynamic = true}, _, _) -> protocol_error(?V_1_0_AMQP_ERROR_NOT_IMPLEMENTED, @@ -1694,7 +1936,7 @@ ensure_target(#'v1_0.target'{address = Address, {ok, Dest} -> QNameBin = ensure_terminus(target, Dest, Vhost, User, Durable), {XNameList1, RK} = rabbit_routing_parser:parse_routing(Dest), - XNameBin = list_to_binary(XNameList1), + XNameBin = unicode:characters_to_binary(XNameList1), XName = rabbit_misc:r(Vhost, exchange, XNameBin), {ok, X} = rabbit_exchange:lookup(XName), check_internal_exchange(X), @@ -1710,7 +1952,7 @@ ensure_target(#'v1_0.target'{address = Address, RoutingKey = case RK of undefined -> undefined; [] -> undefined; - _ -> list_to_binary(RK) + _ -> unicode:characters_to_binary(RK) end, {ok, Exchange, RoutingKey, QNameBin}; {error, _} = E -> @@ -1720,6 +1962,41 @@ ensure_target(#'v1_0.target'{address = Address, {error, {address_not_utf8_string, Address}} end. +handle_outgoing_mgmt_link_flow_control( + #management_link{delivery_count = DeliveryCountSnd} = Link0, + #'v1_0.flow'{handle = Handle = ?UINT(HandleInt), + delivery_count = MaybeDeliveryCountRcv, + link_credit = ?UINT(LinkCreditRcv), + drain = Drain0, + echo = Echo0}, + #state{outgoing_management_links = Links0, + outgoing_pending = Pending + } = State0) -> + Drain = default(Drain0, false), + Echo = default(Echo0, false), + DeliveryCountRcv = delivery_count_rcv(MaybeDeliveryCountRcv), + LinkCreditSnd = link_credit_snd(DeliveryCountRcv, LinkCreditRcv, DeliveryCountSnd), + {Count, Credit} = case Drain of + true -> {add(DeliveryCountSnd, LinkCreditSnd), 0}; + false -> {DeliveryCountSnd, LinkCreditSnd} + end, + State = case Echo orelse Drain of + true -> + Flow = #'v1_0.flow'{ + handle = Handle, + delivery_count = ?UINT(Count), + link_credit = ?UINT(Credit), + available = ?UINT(0), + drain = Drain}, + State0#state{outgoing_pending = queue:in(Flow, Pending)}; + false -> + State0 + end, + Link = Link0#management_link{delivery_count = Count, + credit = Credit}, + Links = maps:update(HandleInt, Link, Links0), + State#state{outgoing_management_links = Links}. + handle_outgoing_link_flow_control( #outgoing_link{queue_name_bin = QNameBin, delivery_count = MaybeDeliveryCountSnd}, @@ -1730,19 +2007,9 @@ handle_outgoing_link_flow_control( echo = Echo0}, State0 = #state{queue_states = QStates0, cfg = #cfg{vhost = Vhost}}) -> - DeliveryCountRcv = case MaybeDeliveryCountRcv of - ?UINT(Count) -> - Count; - undefined -> - %% "In the event that the receiver does not yet know the delivery-count, - %% i.e., delivery-countrcv is unspecified, the sender MUST assume that the - %% delivery-countrcv is the first delivery-countsnd sent from sender to - %% receiver, i.e., the delivery-countsnd specified in the flow state carried - %% by the initial attach frame from the sender to the receiver." [2.6.7] - ?INITIAL_DELIVERY_COUNT - end, - Ctag = handle_to_ctag(HandleInt), QName = rabbit_misc:r(Vhost, queue, QNameBin), + Ctag = handle_to_ctag(HandleInt), + DeliveryCountRcv = delivery_count_rcv(MaybeDeliveryCountRcv), Drain = default(Drain0, false), Echo = default(Echo0, false), case MaybeDeliveryCountSnd of @@ -1755,13 +2022,26 @@ handle_outgoing_link_flow_control( %% thanks to the queue event containing the consumer tag. State; {credit_api_v1, DeliveryCountSnd} -> - LinkCreditSnd = diff(add(DeliveryCountRcv, LinkCreditRcv), DeliveryCountSnd), + LinkCreditSnd = link_credit_snd(DeliveryCountRcv, LinkCreditRcv, DeliveryCountSnd), {ok, QStates, Actions} = rabbit_queue_type:credit_v1(QName, Ctag, LinkCreditSnd, Drain, QStates0), State1 = State0#state{queue_states = QStates}, State = handle_queue_actions(Actions, State1), process_credit_reply_sync(Ctag, QName, LinkCreditSnd, State) end. +delivery_count_rcv(?UINT(DeliveryCount)) -> + DeliveryCount; +delivery_count_rcv(undefined) -> + %% "In the event that the receiver does not yet know the delivery-count, + %% i.e., delivery-countrcv is unspecified, the sender MUST assume that the + %% delivery-countrcv is the first delivery-countsnd sent from sender to + %% receiver, i.e., the delivery-countsnd specified in the flow state carried + %% by the initial attach frame from the sender to the receiver." [2.6.7] + ?INITIAL_DELIVERY_COUNT. + +link_credit_snd(DeliveryCountRcv, LinkCreditRcv, DeliveryCountSnd) -> + diff(add(DeliveryCountRcv, LinkCreditRcv), DeliveryCountSnd). + %% The AMQP 0.9.1 credit extension was poorly designed because a consumer granting %% credits to a queue has to synchronously wait for a credit reply from the queue: %% https://github.com/rabbitmq/rabbitmq-server/blob/b9566f4d02f7ceddd2f267a92d46affd30fb16c8/deps/rabbitmq_codegen/credit_extension.json#L43 @@ -1856,8 +2136,8 @@ ensure_source(#'v1_0.source'{address = Address, true = string:equal(QNameList, QNameBin), {ok, QNameBin}; {XNameList, RoutingKeyList} -> - RoutingKey = list_to_binary(RoutingKeyList), - XNameBin = list_to_binary(XNameList), + RoutingKey = unicode:characters_to_binary(RoutingKeyList), + XNameBin = unicode:characters_to_binary(XNameList), XName = rabbit_misc:r(Vhost, exchange, XNameBin), QName = rabbit_misc:r(Vhost, queue, QNameBin), Binding = #binding{source = XName, @@ -1881,6 +2161,13 @@ ensure_source(#'v1_0.source'{address = Address, {error, {address_not_utf8_string, Address}} end. +transfer_frames(Transfer, Sections, unlimited) -> + [[Transfer, Sections]]; +transfer_frames(Transfer, Sections, MaxFrameSize) -> + %% TODO Ugh + TLen = iolist_size(amqp10_framing:encode_bin(Transfer)), + encode_frames(Transfer, Sections, MaxFrameSize - TLen, []). + encode_frames(_T, _Msg, MaxContentLen, _Transfers) when MaxContentLen =< 0 -> protocol_error(?V_1_0_AMQP_ERROR_FRAME_SIZE_TOO_SMALL, "Frame size is too small by ~tp bytes", @@ -2006,6 +2293,14 @@ validate_multi_transfer_settled(Other, First) "(interpreted) field 'settled' on first transfer (~p)", [Other, First]). +%% "If the message is being sent settled by the sender, +%% the value of this field [rcv-settle-mode] is ignored." [2.7.5] +validate_transfer_rcv_settle_mode(?V_1_0_RECEIVER_SETTLE_MODE_SECOND, _Settled = false) -> + protocol_error(?V_1_0_AMQP_ERROR_NOT_IMPLEMENTED, + "rcv-settle-mode second not supported", []); +validate_transfer_rcv_settle_mode(_, _) -> + ok. + validate_incoming_message_size(Message) -> validate_message_size(Message, persistent_term:get(max_message_size)). @@ -2046,19 +2341,19 @@ ensure_terminus(target, {queue, undefined}, _, _, _) -> %% Default exchange exists. undefined; ensure_terminus(_, {queue, QNameList}, Vhost, User, Durability) -> - declare_queue(list_to_binary(QNameList), Vhost, User, Durability); + declare_queue(unicode:characters_to_binary(QNameList), Vhost, User, Durability); ensure_terminus(_, {amqqueue, QNameList}, Vhost, _, _) -> %% Target "/amq/queue/" is handled specially due to AMQP legacy: %% "Queue names starting with "amq." are reserved for pre-declared and %% standardised queues. The client MAY declare a queue starting with "amq." %% if the passive option is set, or the queue already exists." - QNameBin = list_to_binary(QNameList), + QNameBin = unicode:characters_to_binary(QNameList), ok = exit_if_absent(queue, Vhost, QNameBin), QNameBin. -exit_if_absent(Type, Vhost, Name) -> - ResourceName = rabbit_misc:r(Vhost, Type, rabbit_data_coercion:to_binary(Name)), - Mod = case Type of +exit_if_absent(Kind, Vhost, Name) -> + ResourceName = rabbit_misc:r(Vhost, Kind, unicode:characters_to_binary(Name)), + Mod = case Kind of exchange -> rabbit_exchange; queue -> rabbit_amqqueue end, @@ -2135,14 +2430,6 @@ queue_is_durable(undefined) -> %% [3.5.3] queue_is_durable(?V_1_0_TERMINUS_DURABILITY_NONE). -%% "The two endpoints are not REQUIRED to use the same handle. This means a peer -%% is free to independently chose its handle when a link endpoint is associated -%% with the session. The locally chosen handle is referred to as the output handle. -%% The remotely chosen handle is referred to as the input handle." [2.6.2] -%% For simplicity, we choose to use the same handle. -output_handle(InputHandle) -> - _Outputhandle = InputHandle. - -spec remove_link_from_outgoing_unsettled_map(link_handle() | rabbit_types:ctag(), Map) -> {Map, [rabbit_amqqueue:msg_id()]} when Map :: #{delivery_number() => #outgoing_unsettled{}}. @@ -2205,20 +2492,65 @@ publisher_or_consumer_deleted( %% If we previously already sent a detach with an error condition, and the Detach we %% receive here is therefore the client's reply, do not reply again with a 3rd detach. -maybe_detach_reply(Detach, - #state{incoming_links = NewIncomingLinks, - outgoing_links = NewOutgoingLinks, - cfg = #cfg{writer_pid = WriterPid, - channel_num = Ch}}, - #state{incoming_links = OldIncomingLinks, - outgoing_links = OldOutgoingLinks}) +maybe_detach_reply( + Detach, + #state{incoming_links = NewIncomingLinks, + outgoing_links = NewOutgoingLinks, + incoming_management_links = NewIncomingMgmtLinks, + outgoing_management_links = NewOutgoingMgmtLinks, + cfg = #cfg{writer_pid = WriterPid, + channel_num = Ch}}, + #state{incoming_links = OldIncomingLinks, + outgoing_links = OldOutgoingLinks, + incoming_management_links = OldIncomingMgmtLinks, + outgoing_management_links = OldOutgoingMgmtLinks}) when map_size(NewIncomingLinks) < map_size(OldIncomingLinks) orelse - map_size(NewOutgoingLinks) < map_size(OldOutgoingLinks) -> + map_size(NewOutgoingLinks) < map_size(OldOutgoingLinks) orelse + map_size(NewIncomingMgmtLinks) < map_size(OldIncomingMgmtLinks) orelse + map_size(NewOutgoingMgmtLinks) < map_size(OldOutgoingMgmtLinks) -> Reply = Detach#'v1_0.detach'{error = undefined}, rabbit_amqp_writer:send_command(WriterPid, Ch, Reply); maybe_detach_reply(_, _, _) -> ok. +-spec maybe_detach_mgmt_link(link_handle(), state()) -> state(). +maybe_detach_mgmt_link( + HandleInt, + State = #state{management_link_pairs = LinkPairs0, + incoming_management_links = IncomingLinks0, + outgoing_management_links = OutgoingLinks0}) -> + case maps:take(HandleInt, IncomingLinks0) of + {#management_link{name = Name}, IncomingLinks} -> + Pair = #management_link_pair{outgoing_half = OutgoingHalf} = maps:get(Name, LinkPairs0), + LinkPairs = case OutgoingHalf of + unattached -> + maps:remove(Name, LinkPairs0); + _ -> + maps:update(Name, + Pair#management_link_pair{incoming_half = unattached}, + LinkPairs0) + end, + State#state{incoming_management_links = IncomingLinks, + management_link_pairs = LinkPairs}; + error -> + case maps:take(HandleInt, OutgoingLinks0) of + {#management_link{name = Name}, OutgoingLinks} -> + Pair = #management_link_pair{incoming_half = IncomingHalf} = maps:get(Name, LinkPairs0), + LinkPairs = case IncomingHalf of + unattached -> + maps:remove(Name, LinkPairs0); + _ -> + maps:update(Name, + Pair#management_link_pair{outgoing_half = unattached}, + LinkPairs0) + end, + State#state{outgoing_management_links = OutgoingLinks, + management_link_pairs = LinkPairs}; + error -> + State + end + end. + check_internal_exchange(#exchange{internal = true, name = XName}) -> protocol_error(?V_1_0_AMQP_ERROR_UNAUTHORIZED_ACCESS, @@ -2272,7 +2604,8 @@ check_topic_authorisation(#exchange{type = topic, try rabbit_access_control:check_topic_access(User, Resource, Permission, Context) of ok -> CacheTail = lists:sublist(Cache, ?MAX_PERMISSION_CACHE_SIZE - 1), - put(?TOPIC_PERMISSION_CACHE, [CacheElem | CacheTail]) + put(?TOPIC_PERMISSION_CACHE, [CacheElem | CacheTail]), + ok catch exit:#amqp_error{name = access_refused, explanation = Msg} -> @@ -2309,6 +2642,33 @@ maps_update_with(Key, Fun, Init, Map) -> Map#{Key => Init} end. +max_message_size({ulong, Size}) + when Size > 0 -> + Size; +max_message_size(_) -> + %% "If this field is zero or unset, there is no + %% maximum size imposed by the link endpoint." + unlimited. + +check_paired({map, Properties}) -> + case lists:any(fun({{symbol, <<"paired">>}, true}) -> + true; + (_) -> + false + end, Properties) of + true -> + ok; + false -> + property_paired_not_set() + end; +check_paired(_) -> + property_paired_not_set(). + +-spec property_paired_not_set() -> no_return(). +property_paired_not_set() -> + protocol_error(?V_1_0_AMQP_ERROR_INVALID_FIELD, + "Link property 'paired' is not set to boolean value 'true'", []). + format_status( #{state := #state{cfg = Cfg, outgoing_pending = OutgoingPending, @@ -2320,6 +2680,9 @@ format_status( outgoing_delivery_id = OutgoingDeliveryId, incoming_links = IncomingLinks, outgoing_links = OutgoingLinks, + management_link_pairs = ManagementLinks, + incoming_management_links = IncomingManagementLinks, + outgoing_management_links = OutgoingManagementLinks, outgoing_unsettled_map = OutgoingUnsettledMap, stashed_rejected = StashedRejected, stashed_settled = StashedSettled, @@ -2336,6 +2699,9 @@ format_status( outgoing_delivery_id => OutgoingDeliveryId, incoming_links => IncomingLinks, outgoing_links => OutgoingLinks, + management_link_pairs => ManagementLinks, + incoming_management_links => IncomingManagementLinks, + outgoing_management_links => OutgoingManagementLinks, outgoing_unsettled_map => OutgoingUnsettledMap, stashed_rejected => StashedRejected, stashed_settled => StashedSettled, diff --git a/deps/rabbit/src/rabbit_amqp_util.erl b/deps/rabbit/src/rabbit_amqp_util.erl index 0398c5c38b56..3257cef93704 100644 --- a/deps/rabbit/src/rabbit_amqp_util.erl +++ b/deps/rabbit/src/rabbit_amqp_util.erl @@ -13,7 +13,7 @@ -spec protocol_error(term(), io:format(), [term()]) -> no_return(). protocol_error(Condition, Msg, Args) -> - Description = list_to_binary(lists:flatten(io_lib:format(Msg, Args))), + Description = unicode:characters_to_binary(lists:flatten(io_lib:format(Msg, Args))), Reason = #'v1_0.error'{condition = Condition, description = {utf8, Description}}, exit(Reason). diff --git a/deps/rabbit/src/rabbit_channel.erl b/deps/rabbit/src/rabbit_channel.erl index 8093e655ad9a..2af7fb02e221 100644 --- a/deps/rabbit/src/rabbit_channel.erl +++ b/deps/rabbit/src/rabbit_channel.erl @@ -66,7 +66,8 @@ -export([list_queue_states/1]). %% Mgmt HTTP API refactor --export([handle_method/6]). +-export([handle_method/6, + binding_action/4]). -import(rabbit_misc, [maps_put_truthy/3]). @@ -1819,9 +1820,10 @@ queue_down_consumer_action(CTag, CMap) -> _ -> {recover, ConsumeSpec} end. -binding_action(Fun, SourceNameBin0, DestinationType, DestinationNameBin0, - RoutingKey, Arguments, VHostPath, ConnPid, AuthzContext, - #user{username = Username} = User) -> +binding_action_with_checks( + Action, SourceNameBin0, DestinationType, DestinationNameBin0, + RoutingKey, Arguments, VHostPath, ConnPid, AuthzContext, + #user{username = Username} = User) -> ExchangeNameBin = strip_cr_lf(SourceNameBin0), DestinationNameBin = strip_cr_lf(DestinationNameBin0), DestinationName = name_to_resource(DestinationType, DestinationNameBin, VHostPath), @@ -1835,18 +1837,27 @@ binding_action(Fun, SourceNameBin0, DestinationType, DestinationNameBin0, {ok, Exchange} -> check_read_permitted_on_topic(Exchange, User, RoutingKey, AuthzContext) end, - case Fun(#binding{source = ExchangeName, - destination = DestinationName, - key = RoutingKey, - args = Arguments}, - fun (_X, Q) when ?is_amqqueue(Q) -> - try rabbit_amqqueue:check_exclusive_access(Q, ConnPid) - catch exit:Reason -> {error, Reason} - end; - (_X, #exchange{}) -> - ok - end, - Username) of + Binding = #binding{source = ExchangeName, + destination = DestinationName, + key = RoutingKey, + args = Arguments}, + binding_action(Action, Binding, Username, ConnPid). + +-spec binding_action(add | remove, + rabbit_types:binding(), + rabbit_types:username(), + pid()) -> ok. +binding_action(Action, Binding, Username, ConnPid) -> + case rabbit_binding:Action( + Binding, + fun (_X, Q) when ?is_amqqueue(Q) -> + try rabbit_amqqueue:check_exclusive_access(Q, ConnPid) + catch exit:Reason -> {error, Reason} + end; + (_X, #exchange{}) -> + ok + end, + Username) of {error, {resources_missing, [{not_found, Name} | _]}} -> rabbit_amqqueue:not_found(Name); {error, {resources_missing, [{absent, Q, Reason} | _]}} -> @@ -2375,33 +2386,33 @@ handle_method(#'exchange.bind'{destination = DestinationNameBin, routing_key = RoutingKey, arguments = Arguments}, ConnPid, AuthzContext, _CollectorId, VHostPath, User) -> - binding_action(fun rabbit_binding:add/3, - SourceNameBin, exchange, DestinationNameBin, - RoutingKey, Arguments, VHostPath, ConnPid, AuthzContext, User); + binding_action_with_checks( + add, SourceNameBin, exchange, DestinationNameBin, + RoutingKey, Arguments, VHostPath, ConnPid, AuthzContext, User); handle_method(#'exchange.unbind'{destination = DestinationNameBin, source = SourceNameBin, routing_key = RoutingKey, arguments = Arguments}, ConnPid, AuthzContext, _CollectorId, VHostPath, User) -> - binding_action(fun rabbit_binding:remove/3, - SourceNameBin, exchange, DestinationNameBin, - RoutingKey, Arguments, VHostPath, ConnPid, AuthzContext, User); + binding_action_with_checks( + remove, SourceNameBin, exchange, DestinationNameBin, + RoutingKey, Arguments, VHostPath, ConnPid, AuthzContext, User); handle_method(#'queue.unbind'{queue = QueueNameBin, exchange = ExchangeNameBin, routing_key = RoutingKey, arguments = Arguments}, ConnPid, AuthzContext, _CollectorId, VHostPath, User) -> - binding_action(fun rabbit_binding:remove/3, - ExchangeNameBin, queue, QueueNameBin, - RoutingKey, Arguments, VHostPath, ConnPid, AuthzContext, User); + binding_action_with_checks( + remove, ExchangeNameBin, queue, QueueNameBin, + RoutingKey, Arguments, VHostPath, ConnPid, AuthzContext, User); handle_method(#'queue.bind'{queue = QueueNameBin, exchange = ExchangeNameBin, routing_key = RoutingKey, arguments = Arguments}, ConnPid, AuthzContext, _CollectorId, VHostPath, User) -> - binding_action(fun rabbit_binding:add/3, - ExchangeNameBin, queue, QueueNameBin, - RoutingKey, Arguments, VHostPath, ConnPid, AuthzContext, User); + binding_action_with_checks( + add, ExchangeNameBin, queue, QueueNameBin, + RoutingKey, Arguments, VHostPath, ConnPid, AuthzContext, User); %% Note that all declares to these are effectively passive. If it %% exists it by definition has one consumer. handle_method(#'queue.declare'{queue = <<"amq.rabbitmq.reply-to", @@ -2433,6 +2444,7 @@ handle_method(#'queue.declare'{queue = QueueNameBin, Args0), StrippedQueueNameBin = strip_cr_lf(QueueNameBin), Durable = DurableDeclare andalso not ExclusiveDeclare, + Kind = queue, ActualNameBin = case StrippedQueueNameBin of <<>> -> case rabbit_amqqueue:is_server_named_allowed(Args) of @@ -2444,9 +2456,9 @@ handle_method(#'queue.declare'{queue = QueueNameBin, "Cannot declare a server-named queue for type ~tp", [rabbit_amqqueue:get_queue_type(Args)]) end; - Other -> check_name('queue', Other) + Other -> check_name(Kind, Other) end, - QueueName = rabbit_misc:r(VHostPath, queue, ActualNameBin), + QueueName = rabbit_misc:r(VHostPath, Kind, ActualNameBin), check_configure_permitted(QueueName, User, AuthzContext), rabbit_core_metrics:queue_declared(QueueName), case rabbit_amqqueue:with( @@ -2565,7 +2577,7 @@ handle_method(#'queue.purge'{queue = QueueNameBin}, [rabbit_misc:rs(amqqueue:get_name(Q))]) end end); -handle_method(#'exchange.declare'{exchange = ExchangeNameBin, +handle_method(#'exchange.declare'{exchange = XNameBin, type = TypeNameBin, passive = false, durable = Durable, @@ -2575,13 +2587,14 @@ handle_method(#'exchange.declare'{exchange = ExchangeNameBin, _ConnPid, AuthzContext, _CollectorPid, VHostPath, #user{username = Username} = User) -> CheckedType = rabbit_exchange:check_type(TypeNameBin), - ExchangeName = rabbit_misc:r(VHostPath, exchange, strip_cr_lf(ExchangeNameBin)), + XNameBinStripped = strip_cr_lf(XNameBin), + ExchangeName = rabbit_misc:r(VHostPath, exchange, XNameBinStripped), check_not_default_exchange(ExchangeName), check_configure_permitted(ExchangeName, User, AuthzContext), X = case rabbit_exchange:lookup(ExchangeName) of {ok, FoundX} -> FoundX; {error, not_found} -> - _ = check_name('exchange', strip_cr_lf(ExchangeNameBin)), + _ = check_name('exchange', XNameBinStripped), AeKey = <<"alternate-exchange">>, case rabbit_misc:r_arg(VHostPath, exchange, Args, AeKey) of undefined -> ok; diff --git a/deps/rabbit/src/rabbit_db_exchange.erl b/deps/rabbit/src/rabbit_db_exchange.erl index 2cf2371cbc0c..97fccc2615b2 100644 --- a/deps/rabbit/src/rabbit_db_exchange.erl +++ b/deps/rabbit/src/rabbit_db_exchange.erl @@ -358,7 +358,7 @@ update_in_khepri_tx(Name, Fun) -> -spec create_or_get(Exchange) -> Ret when Exchange :: rabbit_types:exchange(), - Ret :: {new, Exchange} | {existing, Exchange} | {error, any()}. + Ret :: {new, Exchange} | {existing, Exchange}. %% @doc Writes an exchange record if it doesn't exist already or returns %% the existing one. %% diff --git a/deps/rabbit/src/rabbit_exchange.erl b/deps/rabbit/src/rabbit_exchange.erl index 2ed455bfa384..3ecb87ba7eaf 100644 --- a/deps/rabbit/src/rabbit_exchange.erl +++ b/deps/rabbit/src/rabbit_exchange.erl @@ -123,9 +123,7 @@ declare(XName, Type, Durable, AutoDelete, Internal, Args, Username) -> rabbit_event:notify(exchange_created, info(Exchange)), Exchange; {existing, Exchange} -> - Exchange; - Err -> - Err + Exchange end; _ -> rabbit_log:warning("ignoring exchange.declare for exchange ~tp, diff --git a/deps/rabbit/src/rabbit_queue_type.erl b/deps/rabbit/src/rabbit_queue_type.erl index 81f6359727a3..026cf5e57c2b 100644 --- a/deps/rabbit/src/rabbit_queue_type.erl +++ b/deps/rabbit/src/rabbit_queue_type.erl @@ -18,6 +18,7 @@ close/1, discover/1, feature_flag_name/1, + to_binary/1, default/0, is_enabled/1, is_compatible/4, @@ -277,6 +278,16 @@ feature_flag_name(_) -> default() -> rabbit_classic_queue. +-spec to_binary(module()) -> binary(). +to_binary(rabbit_classic_queue) -> + <<"classic">>; +to_binary(rabbit_quorum_queue) -> + <<"quorum">>; +to_binary(rabbit_stream_queue) -> + <<"stream">>; +to_binary(Other) -> + atom_to_binary(Other). + %% is a specific queue type implementation enabled -spec is_enabled(module()) -> boolean(). is_enabled(Type) -> diff --git a/deps/rabbit/src/rabbit_reader.erl b/deps/rabbit/src/rabbit_reader.erl index bb5268450d7e..005c92afc6b3 100644 --- a/deps/rabbit/src/rabbit_reader.erl +++ b/deps/rabbit/src/rabbit_reader.erl @@ -231,7 +231,7 @@ server_properties(Protocol) -> NormalizedConfigServerProps = [{<<"capabilities">>, table, server_capabilities(Protocol)} | [case X of - {KeyAtom, Value} -> {list_to_binary(atom_to_list(KeyAtom)), + {KeyAtom, Value} -> {atom_to_binary(KeyAtom), longstr, maybe_list_to_binary(Value)}; {BinKey, Type, Value} -> {BinKey, Type, Value} diff --git a/deps/rabbit/test/amqp_auth_SUITE.erl b/deps/rabbit/test/amqp_auth_SUITE.erl index eafa0f1cabd4..0aeacd0e1b99 100644 --- a/deps/rabbit/test/amqp_auth_SUITE.erl +++ b/deps/rabbit/test/amqp_auth_SUITE.erl @@ -47,7 +47,25 @@ groups() -> vhost_absent, vhost_connection_limit, user_connection_limit, - vhost_queue_limit + vhost_queue_limit, + + %% AMQP Management operations against HTTP API v2 + declare_exchange, + delete_exchange, + declare_queue, + declare_queue_dlx_queue, + declare_queue_dlx_exchange, + declare_queue_vhost_queue_limit, + delete_queue, + purge_queue, + bind_queue_source, + bind_queue_destination, + bind_exchange_source, + bind_exchange_destination, + bind_to_topic_exchange, + unbind_queue_source, + unbind_queue_target, + unbind_from_topic_exchange ] } ]. @@ -537,6 +555,265 @@ vhost_queue_limit(Config) -> ok = close_connection_sync(C2), ok = rabbit_ct_broker_helpers:clear_vhost_limit(Config, 0, Vhost). +declare_exchange(Config) -> + {Conn, _Session, LinkPair} = init_pair(Config), + XName = <<"๐Ÿ“ฎ"/utf8>>, + ExpectedErr = error_unauthorized( + <<"configure access to exchange '", XName/binary, + "' in vhost 'test vhost' refused for user 'test user'">>), + ?assertEqual({error, {session_ended, ExpectedErr}}, + rabbitmq_amqp_client:declare_exchange(LinkPair, XName, #{})), + ok = close_connection_sync(Conn). + +delete_exchange(Config) -> + {Conn1, _, LinkPair1} = init_pair(Config), + XName = <<"๐Ÿ“ฎ"/utf8>>, + ok = set_permissions(Config, XName, <<>>, <<>>), + ok = rabbitmq_amqp_client:declare_exchange(LinkPair1, XName, #{}), + ok = clear_permissions(Config), + ExpectedErr = error_unauthorized( + <<"configure access to exchange '", XName/binary, + "' in vhost 'test vhost' refused for user 'test user'">>), + ?assertEqual({error, {session_ended, ExpectedErr}}, + rabbitmq_amqp_client:delete_exchange(LinkPair1, XName)), + ok = close_connection_sync(Conn1), + + ok = set_permissions(Config, XName, <<>>, <<>>), + Init = {_, _, LinkPair2} = init_pair(Config), + ok = rabbitmq_amqp_client:delete_exchange(LinkPair2, XName), + ok = cleanup_pair(Init). + +declare_queue(Config) -> + {Conn, _, LinkPair} = init_pair(Config), + QName = <<"๐Ÿฟ"/utf8>>, + ExpectedErr = error_unauthorized( + <<"configure access to queue '", QName/binary, + "' in vhost 'test vhost' refused for user 'test user'">>), + ?assertEqual({error, {session_ended, ExpectedErr}}, + rabbitmq_amqp_client:declare_queue(LinkPair, QName, #{})), + ok = close_connection_sync(Conn). + +declare_queue_dlx_queue(Config) -> + {Conn, _, LinkPair} = init_pair(Config), + QName = <<"๐Ÿฟ"/utf8>>, + DlxName = <<"๐Ÿ“ฅ"/utf8>>, + QProps = #{arguments => #{<<"x-dead-letter-exchange">> => {utf8, DlxName}}}, + %% missing read permission to queue + ok = set_permissions(Config, QName, DlxName, <<>>), + ExpectedErr = error_unauthorized( + <<"read access to queue '", QName/binary, + "' in vhost 'test vhost' refused for user 'test user'">>), + ?assertEqual({error, {session_ended, ExpectedErr}}, + rabbitmq_amqp_client:declare_queue(LinkPair, QName, QProps)), + ok = close_connection_sync(Conn). + +declare_queue_dlx_exchange(Config) -> + {Conn, _, LinkPair} = init_pair(Config), + QName = <<"๐Ÿฟ"/utf8>>, + DlxName = <<"๐Ÿ“ฅ"/utf8>>, + QProps = #{arguments => #{<<"x-dead-letter-exchange">> => {utf8, DlxName}}}, + %% missing write permission to dead letter exchange + ok = set_permissions(Config, QName, <<>>, QName), + ExpectedErr = error_unauthorized( + <<"write access to exchange '", DlxName/binary, + "' in vhost 'test vhost' refused for user 'test user'">>), + ?assertEqual({error, {session_ended, ExpectedErr}}, + rabbitmq_amqp_client:declare_queue(LinkPair, QName, QProps)), + ok = close_connection_sync(Conn). + +declare_queue_vhost_queue_limit(Config) -> + QName = <<"๐Ÿฟ"/utf8>>, + ok = set_permissions(Config, QName, <<>>, <<>>), + Vhost = proplists:get_value(test_vhost, Config), + ok = rabbit_ct_broker_helpers:set_vhost_limit(Config, 0, Vhost, max_queues, 0), + + Init = {_, _, LinkPair} = init_pair(Config), + {error, Resp} = rabbitmq_amqp_client:declare_queue(LinkPair, QName, #{}), + ?assertMatch(#{subject := <<"403">>}, amqp10_msg:properties(Resp)), + ?assertEqual( + #'v1_0.amqp_value'{ + content = {utf8, <<"refused to declare queue '", QName/binary, "' in vhost 'test vhost' ", + "because vhost queue limit 0 is reached">>}}, + amqp10_msg:body(Resp)), + + ok = cleanup_pair(Init), + ok = rabbit_ct_broker_helpers:clear_vhost_limit(Config, 0, Vhost). + +delete_queue(Config) -> + {Conn, _, LinkPair} = init_pair(Config), + QName = <<"๐Ÿฟ"/utf8>>, + ok = set_permissions(Config, QName, <<>>, <<>>), + {ok, _} = rabbitmq_amqp_client:declare_queue(LinkPair, QName, #{}), + ok = clear_permissions(Config), + ExpectedErr = error_unauthorized( + <<"configure access to queue '", QName/binary, + "' in vhost 'test vhost' refused for user 'test user'">>), + ?assertEqual({error, {session_ended, ExpectedErr}}, + rabbitmq_amqp_client:delete_queue(LinkPair, QName)), + ok = close_connection_sync(Conn). + +purge_queue(Config) -> + {Conn, _, LinkPair} = init_pair(Config), + QName = <<"๐Ÿฟ"/utf8>>, + %% missing read permission to queue + ok = set_permissions(Config, QName, <<>>, <<>>), + {ok, _} = rabbitmq_amqp_client:declare_queue(LinkPair, QName, #{}), + ExpectedErr = error_unauthorized( + <<"read access to queue '", QName/binary, + "' in vhost 'test vhost' refused for user 'test user'">>), + ?assertEqual({error, {session_ended, ExpectedErr}}, + rabbitmq_amqp_client:purge_queue(LinkPair, QName)), + ok = close_connection_sync(Conn). + +bind_queue_source(Config) -> + {Conn, _, LinkPair} = init_pair(Config), + QName = atom_to_binary(?FUNCTION_NAME), + %% missing read permission to source exchange + ok = set_permissions(Config, QName, QName, QName), + {ok, #{}} = rabbitmq_amqp_client:declare_queue(LinkPair, QName, #{}), + + XName = <<"amq.direct">>, + ExpectedErr = error_unauthorized( + <<"read access to exchange '", XName/binary, + "' in vhost 'test vhost' refused for user 'test user'">>), + ?assertEqual({error, {session_ended, ExpectedErr}}, + rabbitmq_amqp_client:bind_queue(LinkPair, QName, XName, <<"key">>, #{})), + ok = close_connection_sync(Conn). + +bind_queue_destination(Config) -> + {Conn, _, LinkPair} = init_pair(Config), + QName = <<"my ๐Ÿ‡"/utf8>>, + XName = <<"amq.direct">>, + %% missing write permission to destination queue + ok = set_permissions(Config, QName, <<>>, XName), + {ok, #{}} = rabbitmq_amqp_client:declare_queue(LinkPair, QName, #{}), + + ExpectedErr = error_unauthorized( + <<"write access to queue '", QName/binary, + "' in vhost 'test vhost' refused for user 'test user'">>), + ?assertEqual({error, {session_ended, ExpectedErr}}, + rabbitmq_amqp_client:bind_queue(LinkPair, QName, XName, <<"key">>, #{})), + ok = close_connection_sync(Conn). + +bind_exchange_source(Config) -> + {Conn, _, LinkPair} = init_pair(Config), + SrcXName = <<"amq.fanout">>, + DstXName = <<"amq.direct">>, + %% missing read permission to source exchange + ok = set_permissions(Config, <<>>, DstXName, <<>>), + + ExpectedErr = error_unauthorized( + <<"read access to exchange '", SrcXName/binary, + "' in vhost 'test vhost' refused for user 'test user'">>), + ?assertEqual({error, {session_ended, ExpectedErr}}, + rabbitmq_amqp_client:bind_exchange(LinkPair, DstXName, SrcXName, <<"key">>, #{})), + ok = close_connection_sync(Conn). + +bind_exchange_destination(Config) -> + {Conn, _, LinkPair} = init_pair(Config), + SrcXName = <<"amq.fanout">>, + DstXName = <<"amq.direct">>, + %% missing write permission to destination exchange + ok = set_permissions(Config, <<>>, <<>>, SrcXName), + + ExpectedErr = error_unauthorized( + <<"write access to exchange '", DstXName/binary, + "' in vhost 'test vhost' refused for user 'test user'">>), + ?assertEqual({error, {session_ended, ExpectedErr}}, + rabbitmq_amqp_client:bind_exchange(LinkPair, DstXName, SrcXName, <<"key">>, #{})), + ok = close_connection_sync(Conn). + +bind_to_topic_exchange(Config) -> + {Conn, _, LinkPair} = init_pair(Config), + SrcXName = <<"amq.topic">>, + DstXName = <<"amq.direct">>, + Topic = <<"a.b.๐Ÿ‡"/utf8>>, + + User = ?config(test_user, Config), + Vhost = ?config(test_vhost, Config), + ok = rabbit_ct_broker_helpers:set_full_permissions(Config, User, Vhost), + %% missing read permission to Topic + ok = set_topic_permissions(Config, SrcXName, <<".*">>, <<"wrong.topic">>), + + ExpectedErr = error_unauthorized( + <<"read access to topic '", Topic/binary, + "' in exchange 'amq.topic' in vhost 'test vhost' refused for user 'test user'">>), + ?assertEqual({error, {session_ended, ExpectedErr}}, + rabbitmq_amqp_client:bind_exchange(LinkPair, DstXName, SrcXName, Topic, #{})), + ok = close_connection_sync(Conn). + +unbind_queue_source(Config) -> + {Conn, _, LinkPair} = init_pair(Config), + QName = BindingKey = atom_to_binary(?FUNCTION_NAME), + XName = <<"amq.direct">>, + ok = set_permissions(Config, QName, QName, XName), + {ok, #{}} = rabbitmq_amqp_client:declare_queue(LinkPair, QName, #{}), + ok = rabbitmq_amqp_client:bind_queue(LinkPair, QName, XName, BindingKey, #{}), + + %% remove read permission to source exchange + ok = set_permissions(Config, QName, QName, <<"^$">>), + ExpectedErr = error_unauthorized( + <<"read access to exchange '", XName/binary, + "' in vhost 'test vhost' refused for user 'test user'">>), + ?assertEqual({error, {session_ended, ExpectedErr}}, + rabbitmq_amqp_client:unbind_queue(LinkPair, QName, XName, BindingKey, #{})), + ok = close_connection_sync(Conn). + +unbind_queue_target(Config) -> + {Conn, _, LinkPair} = init_pair(Config), + QName = BindingKey = atom_to_binary(?FUNCTION_NAME), + XName = <<"amq.direct">>, + ok = set_permissions(Config, QName, QName, XName), + {ok, #{}} = rabbitmq_amqp_client:declare_queue(LinkPair, QName, #{}), + ok = rabbitmq_amqp_client:bind_queue(LinkPair, QName, XName, BindingKey, #{}), + + %% remove write permission to destination queue + ok = set_permissions(Config, QName, <<"^$">>, XName), + ExpectedErr = error_unauthorized( + <<"write access to queue '", QName/binary, + "' in vhost 'test vhost' refused for user 'test user'">>), + ?assertEqual({error, {session_ended, ExpectedErr}}, + rabbitmq_amqp_client:unbind_queue(LinkPair, QName, XName, BindingKey, #{})), + ok = close_connection_sync(Conn). + +unbind_from_topic_exchange(Config) -> + Init = {_, _, LinkPair1} = init_pair(Config), + SrcXName = <<"amq.topic">>, + DstXName = <<"amq.direct">>, + Topic = <<"a.b.๐Ÿ‡"/utf8>>, + + User = ?config(test_user, Config), + Vhost = ?config(test_vhost, Config), + ok = rabbit_ct_broker_helpers:set_full_permissions(Config, User, Vhost), + ok = set_topic_permissions(Config, SrcXName, <<"^$">>, Topic), + ok = rabbitmq_amqp_client:bind_exchange(LinkPair1, DstXName, SrcXName, Topic, #{}), + + %% remove Topic read permission + ok = set_topic_permissions(Config, SrcXName, <<"^$">>, <<"^$">>), + %% Start a new connection since topic permissions are cached by the AMQP session process. + ok = cleanup_pair(Init), + {Conn, _, LinkPair2} = init_pair(Config), + + ExpectedErr = error_unauthorized( + <<"read access to topic '", Topic/binary, + "' in exchange 'amq.topic' in vhost 'test vhost' refused for user 'test user'">>), + ?assertEqual({error, {session_ended, ExpectedErr}}, + rabbitmq_amqp_client:unbind_exchange(LinkPair2, DstXName, SrcXName, Topic, #{})), + + ok = close_connection_sync(Conn). + +init_pair(Config) -> + OpnConf = connection_config(Config), + {ok, Connection} = amqp10_client:open_connection(OpnConf), + {ok, Session} = amqp10_client:begin_session_sync(Connection), + {ok, LinkPair} = rabbitmq_amqp_client:attach_management_link_pair_sync(Session, <<"mgmt link pair">>), + {Connection, Session, LinkPair}. + +cleanup_pair({Connection, Session, LinkPair}) -> + ok = rabbitmq_amqp_client:detach_management_link_pair_sync(LinkPair), + ok = amqp10_client:end_session(Session), + ok = amqp10_client:close_connection(Connection). + connection_config(Config) -> Vhost = ?config(test_vhost, Config), connection_config(Config, Vhost). diff --git a/deps/rabbit/test/amqp_client_SUITE.erl b/deps/rabbit/test/amqp_client_SUITE.erl index 5e9da821f31b..0d1deaf78566 100644 --- a/deps/rabbit/test/amqp_client_SUITE.erl +++ b/deps/rabbit/test/amqp_client_SUITE.erl @@ -167,8 +167,7 @@ init_per_testcase(T, Config) T =:= roundtrip_with_drain_quorum_queue orelse T =:= timed_get_quorum_queue orelse T =:= available_messages_quorum_queue -> - case rabbit_ct_broker_helpers:rpc( - Config, rabbit_feature_flags, is_enabled, [credit_api_v2]) of + case rpc(Config, rabbit_feature_flags, is_enabled, [credit_api_v2]) of true -> rabbit_ct_helpers:testcase_started(Config, T); false -> @@ -221,19 +220,16 @@ reliable_send_receive(QType, Outcome, Config) -> end, ct:pal("~s testing ~s", [?FUNCTION_NAME, OutcomeBin]), - QName = <>, - Ch = rabbit_ct_client_helpers:open_channel(Config), - #'queue.declare_ok'{} = amqp_channel:call( - Ch, #'queue.declare'{ - queue = QName, - durable = true, - arguments = [{<<"x-queue-type">>, longstr, QType}]}), - ok = rabbit_ct_client_helpers:close_channel(Ch), - - %% reliable send and consume OpnConf = connection_config(Config), {ok, Connection} = amqp10_client:open_connection(OpnConf), {ok, Session} = amqp10_client:begin_session_sync(Connection), + {ok, LinkPair} = rabbitmq_amqp_client:attach_management_link_pair_sync(Session, <<"pair">>), + QName = <>, + QProps = #{arguments => #{<<"x-queue-type">> => {utf8, QType}}}, + {ok, _} = rabbitmq_amqp_client:declare_queue(LinkPair, QName, QProps), + ok = rabbitmq_amqp_client:detach_management_link_pair_sync(LinkPair), + + %% reliable send and consume Address = <<"/amq/queue/", QName/binary>>, {ok, Sender} = amqp10_client:attach_sender_link( Session, <<"test-sender">>, Address), @@ -268,24 +264,17 @@ reliable_send_receive(QType, Outcome, Config) -> flush("post accept"), ok = amqp10_client:detach_link(Receiver), + ok = delete_queue(Session2, QName), ok = end_session_sync(Session2), - ok = amqp10_client:close_connection(Connection2), - ok = delete_queue(Config, QName). + ok = amqp10_client:close_connection(Connection2). %% Tests that confirmations are returned correctly %% when sending many messages async to a quorum queue. sender_settle_mode_unsettled(Config) -> QName = atom_to_binary(?FUNCTION_NAME), - Ch = rabbit_ct_client_helpers:open_channel(Config), - #'queue.declare_ok'{} = amqp_channel:call( - Ch, #'queue.declare'{ - queue = QName, - durable = true, - arguments = [{<<"x-queue-type">>, longstr, <<"quorum">>}]}), - - OpnConf = connection_config(Config), - {ok, Connection} = amqp10_client:open_connection(OpnConf), - {ok, Session} = amqp10_client:begin_session_sync(Connection), + {Connection, Session, LinkPair} = init(Config), + QProps = #{arguments => #{<<"x-queue-type">> => {utf8, <<"quorum">>}}}, + {ok, _} = rabbitmq_amqp_client:declare_queue(LinkPair, QName, QProps), Address = <<"/amq/queue/", QName/binary>>, {ok, Sender} = amqp10_client:attach_sender_link( Session, <<"test-sender">>, Address, unsettled), @@ -306,24 +295,20 @@ sender_settle_mode_unsettled(Config) -> end || DTag <- DTags], ok = amqp10_client:detach_link(Sender), + ?assertMatch({ok, #{message_count := NumMsgs}}, + rabbitmq_amqp_client:delete_queue(LinkPair, QName)), + ok = rabbitmq_amqp_client:detach_management_link_pair_sync(LinkPair), ok = end_session_sync(Session), - ok = amqp10_client:close_connection(Connection), - ?assertEqual(#'queue.delete_ok'{message_count = NumMsgs}, - amqp_channel:call(Ch, #'queue.delete'{queue = QName})), - ok = rabbit_ct_client_helpers:close_channel(Ch). + ok = amqp10_client:close_connection(Connection). sender_settle_mode_unsettled_fanout(Config) -> + {Connection, Session, LinkPair} = init(Config), QNames = [<<"q1">>, <<"q2">>, <<"q3">>], - Ch = rabbit_ct_client_helpers:open_channel(Config), [begin - #'queue.declare_ok'{} = amqp_channel:call(Ch, #'queue.declare'{queue = QName}), - #'queue.bind_ok'{} = amqp_channel:call(Ch, #'queue.bind'{queue = QName, - exchange = <<"amq.fanout">>}) + {ok, _} = rabbitmq_amqp_client:declare_queue(LinkPair, QName, #{}), + ok = rabbitmq_amqp_client:bind_queue(LinkPair, QName, <<"amq.fanout">>, <<>>, #{}) end || QName <- QNames], - OpnConf = connection_config(Config), - {ok, Connection} = amqp10_client:open_connection(OpnConf), - {ok, Session} = amqp10_client:begin_session_sync(Connection), Address = <<"/exchange/amq.fanout">>, {ok, Sender} = amqp10_client:attach_sender_link(Session, <<"test-sender">>, Address, unsettled), ok = wait_for_credit(Sender), @@ -343,28 +328,22 @@ sender_settle_mode_unsettled_fanout(Config) -> end || DTag <- DTags], ok = amqp10_client:detach_link(Sender), + [?assertMatch({ok, #{message_count := NumMsgs}}, + rabbitmq_amqp_client:delete_queue(LinkPair, QName)) + || QName <- QNames], + ok = rabbitmq_amqp_client:detach_management_link_pair_sync(LinkPair), ok = end_session_sync(Session), - ok = amqp10_client:close_connection(Connection), - [?assertEqual(#'queue.delete_ok'{message_count = NumMsgs}, - amqp_channel:call(Ch, #'queue.delete'{queue = QName})) || - QName <- QNames], - ok = rabbit_ct_client_helpers:close_channel(Ch). + ok = amqp10_client:close_connection(Connection). %% Tests that confirmations are returned correctly %% when sending many messages async to a quorum queue where %% every 3rd message is settled by the sender. sender_settle_mode_mixed(Config) -> + {Connection, Session, LinkPair} = init(Config), QName = atom_to_binary(?FUNCTION_NAME), - Ch = rabbit_ct_client_helpers:open_channel(Config), - #'queue.declare_ok'{} = amqp_channel:call( - Ch, #'queue.declare'{ - queue = QName, - durable = true, - arguments = [{<<"x-queue-type">>, longstr, <<"quorum">>}]}), + QProps = #{arguments => #{<<"x-queue-type">> => {utf8, <<"quorum">>}}}, + {ok, _} = rabbitmq_amqp_client:declare_queue(LinkPair, QName, QProps), - OpnConf = connection_config(Config), - {ok, Connection} = amqp10_client:open_connection(OpnConf), - {ok, Session} = amqp10_client:begin_session_sync(Connection), Address = <<"/amq/queue/", QName/binary>>, {ok, Sender} = amqp10_client:attach_sender_link( Session, <<"test-sender">>, Address, mixed), @@ -391,27 +370,20 @@ sender_settle_mode_mixed(Config) -> end || DTag <- DTags], ok = amqp10_client:detach_link(Sender), + ?assertMatch({ok, #{message_count := NumMsgs}}, + rabbitmq_amqp_client:delete_queue(LinkPair, QName)), + ok = rabbitmq_amqp_client:detach_management_link_pair_sync(LinkPair), ok = end_session_sync(Session), - ok = amqp10_client:close_connection(Connection), - ?assertEqual(#'queue.delete_ok'{message_count = NumMsgs}, - amqp_channel:call(Ch, #'queue.delete'{queue = QName})), - ok = rabbit_ct_client_helpers:close_channel(Ch). + ok = amqp10_client:close_connection(Connection). quorum_queue_rejects(Config) -> + {Connection, Session, LinkPair} = init(Config), QName = atom_to_binary(?FUNCTION_NAME), - Ch = rabbit_ct_client_helpers:open_channel(Config), - #'queue.declare_ok'{} = amqp_channel:call( - Ch, #'queue.declare'{ - queue = QName, - durable = true, - arguments = [{<<"x-queue-type">>, longstr, <<"quorum">>}, - {<<"x-max-length">>, long, 1}, - {<<"x-overflow">>, longstr, <<"reject-publish">>} - ]}), + QProps = #{arguments => #{<<"x-queue-type">> => {utf8, <<"quorum">>}, + <<"x-max-length">> => {ulong, 1}, + <<"x-overflow">> => {utf8, <<"reject-publish">>}}}, + {ok, _} = rabbitmq_amqp_client:declare_queue(LinkPair, QName, QProps), - OpnConf = connection_config(Config), - {ok, Connection} = amqp10_client:open_connection(OpnConf), - {ok, Session} = amqp10_client:begin_session_sync(Connection), Address = <<"/amq/queue/", QName/binary>>, {ok, Sender} = amqp10_client:attach_sender_link( Session, <<"test-sender">>, Address, mixed), @@ -444,12 +416,11 @@ quorum_queue_rejects(Config) -> end || DTag <- DTags ++ [<<"tag d">>]], ok = amqp10_client:detach_link(Sender), + ?assertMatch({ok, #{message_count := 2}}, + rabbitmq_amqp_client:delete_queue(LinkPair, QName)), + ok = rabbitmq_amqp_client:detach_management_link_pair_sync(LinkPair), ok = amqp10_client:end_session(Session), - ok = amqp10_client:close_connection(Connection), - - ?assertEqual(#'queue.delete_ok'{message_count = 2}, - amqp_channel:call(Ch, #'queue.delete'{queue = QName})), - ok = rabbit_ct_client_helpers:close_channel(Ch). + ok = amqp10_client:close_connection(Connection). receiver_settle_mode_first(Config) -> QName = atom_to_binary(?FUNCTION_NAME), @@ -536,9 +507,9 @@ receiver_settle_mode_first(Config) -> assert_messages(QName, 0, 0, Config), ok = amqp10_client:detach_link(Receiver), + ok = delete_queue(Session, QName), ok = amqp10_client:end_session(Session), - ok = amqp10_client:close_connection(Connection), - ok = delete_queue(Config, QName). + ok = amqp10_client:close_connection(Connection). publishing_to_non_existing_queue_should_settle_with_released(Config) -> OpnConf = connection_config(Config), @@ -593,16 +564,9 @@ roundtrip_with_drain_stream(Config) -> roundtrip_with_drain(Config, QueueType, QName) when is_binary(QueueType) -> Address = <<"/amq/queue/", QName/binary>>, - Ch = rabbit_ct_client_helpers:open_channel(Config), - Args = [{<<"x-queue-type">>, longstr, QueueType}], - #'queue.declare_ok'{} = amqp_channel:call( - Ch, #'queue.declare'{ - queue = QName, - durable = true, - arguments = Args}), - OpnConf = connection_config(Config), - {ok, Connection} = amqp10_client:open_connection(OpnConf), - {ok, Session} = amqp10_client:begin_session_sync(Connection), + {Connection, Session, LinkPair} = init(Config), + QProps = #{arguments => #{<<"x-queue-type">> => {utf8, QueueType}}}, + {ok, _} = rabbitmq_amqp_client:declare_queue(LinkPair, QName, QProps), {ok, Sender} = amqp10_client:attach_sender_link( Session, <<"test-sender">>, Address), wait_for_credit(Sender), @@ -641,24 +605,19 @@ roundtrip_with_drain(Config, QueueType, QName) flush("final"), ok = amqp10_client:detach_link(Sender), - ok = amqp10_client:close_connection(Connection), - ok = delete_queue(Config, QName). + {ok, _} = rabbitmq_amqp_client:delete_queue(LinkPair, QName), + ok = rabbitmq_amqp_client:detach_management_link_pair_sync(LinkPair), + ok = amqp10_client:close_connection(Connection). %% Send a message with a body containing a single AMQP 1.0 value section %% to a stream and consume via AMQP 0.9.1. amqp_stream_amqpl(Config) -> - Ch = rabbit_ct_client_helpers:open_channel(Config), + {Connection, Session, LinkPair} = init(Config), QName = atom_to_binary(?FUNCTION_NAME), - - amqp_channel:call(Ch, #'queue.declare'{ - queue = QName, - durable = true, - arguments = [{<<"x-queue-type">>, longstr, <<"stream">>}]}), + QProps = #{arguments => #{<<"x-queue-type">> => {utf8, <<"stream">>}}}, + {ok, #{type := <<"stream">>}} = rabbitmq_amqp_client:declare_queue(LinkPair, QName, QProps), Address = <<"/amq/queue/", QName/binary>>, - OpnConf = connection_config(Config), - {ok, Connection} = amqp10_client:open_connection(OpnConf), - {ok, Session} = amqp10_client:begin_session_sync(Connection), {ok, Sender} = amqp10_client:attach_sender_link( Session, <<"test-sender">>, Address), wait_for_credit(Sender), @@ -666,8 +625,8 @@ amqp_stream_amqpl(Config) -> ok = amqp10_client:send_msg(Sender, OutMsg), flush("final"), ok = amqp10_client:detach_link(Sender), - ok = amqp10_client:close_connection(Connection), + Ch = rabbit_ct_client_helpers:open_channel(Config), #'basic.qos_ok'{} = amqp_channel:call(Ch, #'basic.qos'{global = false, prefetch_count = 1}), CTag = <<"my-tag">>, @@ -686,8 +645,11 @@ amqp_stream_amqpl(Config) -> after 5000 -> ct:fail(basic_deliver_timeout) end, - #'queue.delete_ok'{} = amqp_channel:call(Ch, #'queue.delete'{queue = QName}), - ok = rabbit_ct_client_helpers:close_channel(Ch). + + ok = rabbit_ct_client_helpers:close_channel(Ch), + {ok, _} = rabbitmq_amqp_client:delete_queue(LinkPair, QName), + ok = rabbitmq_amqp_client:detach_management_link_pair_sync(LinkPair), + ok = amqp10_client:close_connection(Connection). message_headers_conversion(Config) -> QName = atom_to_binary(?FUNCTION_NAME), @@ -706,7 +668,7 @@ message_headers_conversion(Config) -> amqp091_to_amqp10_header_conversion(Session, Ch, QName, Address), ok = rabbit_ct_client_helpers:close_channel(Ch), - ok = delete_queue(Config, QName), + ok = delete_queue(Session, QName), ok = amqp10_client:close_connection(Connection). amqp10_to_amqp091_header_conversion(Session,Ch, QName, Address) -> @@ -836,10 +798,10 @@ multiple_sessions(Config) -> %% Clean up. [ok = amqp10_client:detach_link(Link) || Link <- [Receiver1, Receiver2, Sender1, Sender2]], + [ok = delete_queue(Session1, Q) || Q <- Qs], ok = end_session_sync(Session1), ok = end_session_sync(Session2), - ok = amqp10_client:close_connection(Connection), - [ok = delete_queue(Config, Q) || Q <- Qs]. + ok = amqp10_client:close_connection(Connection). server_closes_link_classic_queue(Config) -> server_closes_link(<<"classic">>, Config). @@ -897,7 +859,7 @@ server_closes_link(QType, Config) -> %% Server closes the link endpoint due to some AMQP 1.0 external condition: %% In this test, the external condition is that an AMQP 0.9.1 client deletes the queue. - delete_queue(Config, QName), + ok = delete_queue(Session, QName), %% We expect that the server closes the link endpoints, %% i.e. the server sends us DETACH frames. @@ -1012,7 +974,7 @@ link_target_queue_deleted(QType, Config) -> %% Now, the server AMQP session contains a delivery that did not get confirmed by the target queue. %% If we now delete that target queue, RabbitMQ must not reply to us with ACCEPTED. %% Instead, we expect RabbitMQ to reply with RELEASED since no queue ever received our 2nd message. - delete_queue(Config, QName), + ok = delete_queue(Session, QName), ok = wait_for_settlement(DTag2, released), %% After the 2nd message got released, we additionally expect RabbitMQ to close the link given @@ -1136,7 +1098,7 @@ events(Config) -> Pid = proplists:lookup(pid, Props), ClientProperties = {client_properties, List} = proplists:lookup(client_properties, Props), ?assert(lists:member( - {<<"product">>, longstr, <<"AMQP 1.0 client from the RabbitMQ Project">>}, + {<<"product">>, longstr, <<"AMQP 1.0 client">>}, List)), ?assert(lists:member( {<<"ignore-maintenance">>, bool, true}, @@ -1581,19 +1543,12 @@ single_active_consumer_quorum_queue(Config) -> single_active_consumer(QType, Config) -> QName = atom_to_binary(?FUNCTION_NAME), - Ch = rabbit_ct_client_helpers:open_channel(Config), - #'queue.declare_ok'{} = amqp_channel:call( - Ch, #'queue.declare'{ - queue = QName, - durable = true, - arguments = [{<<"x-single-active-consumer">>, bool, true}, - {<<"x-queue-type">>, longstr, QType}]}), - ok = rabbit_ct_client_helpers:close_channel(Ch), + {Connection, Session, LinkPair} = init(Config), + QProps = #{arguments => #{<<"x-queue-type">> => {utf8, QType}, + <<"x-single-active-consumer">> => true}}, + {ok, #{type := QType}} = rabbitmq_amqp_client:declare_queue(LinkPair, QName, QProps), %% Attach 1 sender and 2 receivers to the queue. - OpnConf = connection_config(Config), - {ok, Connection} = amqp10_client:open_connection(OpnConf), - {ok, Session} = amqp10_client:begin_session_sync(Connection), Address = <<"/amq/queue/", QName/binary>>, {ok, Sender} = amqp10_client:attach_sender_link( Session, <<"test-sender">>, Address), @@ -1661,9 +1616,10 @@ single_active_consumer(QType, Config) -> end, ok = amqp10_client:detach_link(Receiver2), + {ok, _} = rabbitmq_amqp_client:delete_queue(LinkPair, QName), + ok = rabbitmq_amqp_client:detach_management_link_pair_sync(LinkPair), ok = end_session_sync(Session), - ok = amqp10_client:close_connection(Connection), - delete_queue(Config, QName). + ok = amqp10_client:close_connection(Connection). %% "A session endpoint can choose to unmap its output handle for a link. In this case, the endpoint MUST %% send a detach frame to inform the remote peer that the handle is no longer attached to the link endpoint. @@ -2048,9 +2004,9 @@ receive_transfer_flow_order(Config) -> after 5000 -> ct:fail("timeout receiving credit_exhausted") end, + ok = delete_queue(Session, QName), ok = end_session_sync(Session), - ok = amqp10_client:close_connection(Connection), - ok = delete_queue(Config, QName). + ok = amqp10_client:close_connection(Connection). last_queue_confirms(Config) -> ClassicQ = <<"my classic queue">>, @@ -2255,9 +2211,9 @@ target_classic_queue_down(Config) -> ok = amqp10_client:detach_link(Sender), ok = amqp10_client:detach_link(Receiver2), + ok = delete_queue(Session, QName), ok = end_session_sync(Session), - ok = amqp10_client:close_connection(Connection), - delete_queue(Config, QName). + ok = amqp10_client:close_connection(Connection). async_notify_settled_classic_queue(Config) -> %% TODO Bump old version in mixed version tests to 3.13.x, @@ -2322,9 +2278,8 @@ async_notify(SenderSettleMode, QType, Config) -> %% If it is a stream we need to wait until there is a local member %% on the node we want to subscibe from before proceeding. rabbit_ct_helpers:await_condition( - fun() -> rabbit_ct_broker_helpers:rpc( - Config, 0, ?MODULE, has_local_member, - [rabbit_misc:r(<<"/">>, queue, QName)]) + fun() -> rpc(Config, 0, ?MODULE, has_local_member, + [rabbit_misc:r(<<"/">>, queue, QName)]) end, 30_000); _ -> ok @@ -2439,10 +2394,10 @@ link_flow_control(Config) -> end, [ok = amqp10_client:detach_link(Link) || Link <- [ReceiverCQ, ReceiverQQ, SenderCQ, SenderQQ]], + ok = delete_queue(Session, QQ), + ok = delete_queue(Session, CQ), ok = end_session_sync(Session), - ok = amqp10_client:close_connection(Connection), - delete_queue(Config, QQ), - delete_queue(Config, CQ). + ok = amqp10_client:close_connection(Connection). classic_queue_on_old_node(Config) -> %% TODO Bump old version in mixed version tests to 3.13.x, @@ -2860,7 +2815,7 @@ stream_filtering(Config) -> ?assertEqual(WaveCount * 2, length(AppleUnfilteredFilteredMessages)), ok = amqp10_client:detach_link(AppleUnfilteredReceiver), - delete_queue(Config, Stream), + ok = delete_queue(Session, Stream), ok = amqp10_client:close_connection(Connection). available_messages_classic_queue(Config) -> @@ -2968,9 +2923,9 @@ incoming_message_interceptors(Config) -> ok = amqp10_client:detach_link(Sender), ok = amqp10_client:detach_link(Receiver), + ok = delete_queue(Session, QName), ok = end_session_sync(Session), ok = amqp10_client:close_connection(Connection), - delete_queue(Config, QName), true = rpc(Config, persistent_term, erase, [Key]). trace(Config) -> @@ -3052,10 +3007,10 @@ trace(Config) -> ok = amqp10_client:detach_link(Sender), ok = amqp10_client:detach_link(Receiver), + [delete_queue(SessionSender, Q0) || Q0 <- Qs], ok = end_session_sync(SessionSender), ok = end_session_sync(SessionReceiver), - ok = amqp10_client:close_connection(Connection), - [delete_queue(Config, Q0) || Q0 <- Qs]. + ok = amqp10_client:close_connection(Connection). %% https://www.rabbitmq.com/validated-user-id.html user_id(Config) -> @@ -3125,8 +3080,8 @@ message_ttl(Config) -> ok = amqp10_client:detach_link(Sender), ok = amqp10_client:detach_link(Receiver), - ok = amqp10_client:close_connection(Connection), - ok = delete_queue(Config, QName). + ok = delete_queue(Session, QName), + ok = amqp10_client:close_connection(Connection). %% For backward compatibility, deployment tools should be able to %% enable and disable the deprecated no-op AMQP 1.0 plugin. @@ -3344,13 +3299,20 @@ classic_priority_queue(Config) -> ok = amqp10_client:detach_link(Receiver1), ok = amqp10_client:detach_link(Receiver2), ok = amqp10_client:detach_link(Sender), + ok = delete_queue(Session, QName), ok = end_session_sync(Session), - ok = amqp10_client:close_connection(Connection), - ok = delete_queue(Config, QName). + ok = amqp10_client:close_connection(Connection). %% internal %% +init(Config) -> + OpnConf = connection_config(Config), + {ok, Connection} = amqp10_client:open_connection(OpnConf), + {ok, Session} = amqp10_client:begin_session_sync(Connection), + {ok, LinkPair} = rabbitmq_amqp_client:attach_management_link_pair_sync(Session, <<"my link pair">>), + {Connection, Session, LinkPair}. + receive_all_messages(Receiver, Accept) -> receive_all_messages0(Receiver, Accept, []). @@ -3467,10 +3429,11 @@ wait_for_accepts(N) -> ct:fail({missing_accepted, N}) end. -delete_queue(Config, QName) -> - Ch = rabbit_ct_client_helpers:open_channel(Config), - #'queue.delete_ok'{} = amqp_channel:call(Ch, #'queue.delete'{queue = QName}), - ok = rabbit_ct_client_helpers:close_channel(Ch). +delete_queue(Session, QName) -> + {ok, LinkPair} = rabbitmq_amqp_client:attach_management_link_pair_sync( + Session, <<"delete queue">>), + {ok, _} = rabbitmq_amqp_client:delete_queue(LinkPair, QName), + ok = rabbitmq_amqp_client:detach_management_link_pair_sync(LinkPair). amqp091_get_msg_headers(Channel, QName) -> {#'basic.get_ok'{}, #amqp_msg{props = #'P_basic'{ headers= Headers}}} diff --git a/deps/rabbit_common/src/rabbit_routing_parser.erl b/deps/rabbit_common/src/rabbit_routing_parser.erl index 59512c3785a9..fa92d3943bcf 100644 --- a/deps/rabbit_common/src/rabbit_routing_parser.erl +++ b/deps/rabbit_common/src/rabbit_routing_parser.erl @@ -21,7 +21,7 @@ parse_endpoint(Destination, AllowAnonymousQueue) when is_binary(Destination) -> parse_endpoint(unicode:characters_to_list(Destination), AllowAnonymousQueue); parse_endpoint(Destination, AllowAnonymousQueue) when is_list(Destination) -> - case re:split(Destination, "/", [{return, list}]) of + case re:split(Destination, "/", [unicode, {return, list}]) of [Name] -> {ok, {queue, unescape(Name)}}; ["", Type | Rest] diff --git a/deps/rabbitmq_amqp_client/.gitignore b/deps/rabbitmq_amqp_client/.gitignore new file mode 100644 index 000000000000..0de8bdab4c4f --- /dev/null +++ b/deps/rabbitmq_amqp_client/.gitignore @@ -0,0 +1,17 @@ +.sw? +.*.sw? +*.beam +/.erlang.mk/ +/cover/ +/deps/ +/doc/ +/ebin/ +/escript/ +/escript.lock +/logs/ +/plugins/ +/plugins.lock +/sbin/ +/sbin.lock + +/rabbitmq_amqp_client.d diff --git a/deps/rabbitmq_amqp_client/BUILD.bazel b/deps/rabbitmq_amqp_client/BUILD.bazel new file mode 100644 index 000000000000..796bd653e1f3 --- /dev/null +++ b/deps/rabbitmq_amqp_client/BUILD.bazel @@ -0,0 +1,91 @@ +load("@rules_erlang//:eunit2.bzl", "eunit") +load("@rules_erlang//:xref2.bzl", "xref") +load("@rules_erlang//:dialyze.bzl", "dialyze", "plt") +load( + "//:rabbitmq.bzl", + "RABBITMQ_DIALYZER_OPTS", + "assert_suites", + "broker_for_integration_suites", + "rabbitmq_app", + "rabbitmq_integration_suite", + "rabbitmq_suite", +) +load( + ":app.bzl", + "all_beam_files", + "all_srcs", + "all_test_beam_files", + "test_suite_beam_files", +) + +APP_NAME = "rabbitmq_amqp_client" + +APP_DESCRIPTION = "AMQP 1.0 client for RabbitMQ" + +all_beam_files(name = "all_beam_files") + +all_test_beam_files(name = "all_test_beam_files") + +all_srcs(name = "all_srcs") + +test_suite_beam_files(name = "test_suite_beam_files") + +rabbitmq_app( + name = "erlang_app", + srcs = [":all_srcs"], + hdrs = [":public_hdrs"], + app_description = APP_DESCRIPTION, + app_name = APP_NAME, + beam_files = [":beam_files"], + license_files = [":license_files"], + priv = [":priv"], + deps = [ + "//deps/amqp10_client:erlang_app", + "//deps/amqp10_common:erlang_app", + ], +) + +xref( + name = "xref", + target = ":erlang_app", +) + +plt( + name = "deps_plt", + for_target = ":erlang_app", + plt = "//:base_plt", +) + +dialyze( + name = "dialyze", + dialyzer_opts = RABBITMQ_DIALYZER_OPTS, + plt = ":deps_plt", + target = ":erlang_app", +) + +broker_for_integration_suites( +) + +TEST_DEPS = [ + "//deps/amqp10_client:erlang_app", +] + +rabbitmq_integration_suite( + name = "management_SUITE", + size = "medium", + shard_count = 2, + deps = TEST_DEPS, +) + +assert_suites() + +alias( + name = "rabbitmq_amqp_client", + actual = ":erlang_app", + visibility = ["//visibility:public"], +) + +eunit( + name = "eunit", + target = ":test_erlang_app", +) diff --git a/deps/rabbitmq_amqp_client/LICENSE b/deps/rabbitmq_amqp_client/LICENSE new file mode 100644 index 000000000000..1699234a3e89 --- /dev/null +++ b/deps/rabbitmq_amqp_client/LICENSE @@ -0,0 +1,4 @@ +This package is licensed under the MPL 2.0. For the MPL 2.0, please see LICENSE-MPL-RabbitMQ. + +If you have any questions regarding licensing, please contact us at +rabbitmq-core@groups.vmware.com. diff --git a/deps/rabbitmq_amqp_client/LICENSE-MPL-RabbitMQ b/deps/rabbitmq_amqp_client/LICENSE-MPL-RabbitMQ new file mode 100644 index 000000000000..14e2f777f6c3 --- /dev/null +++ b/deps/rabbitmq_amqp_client/LICENSE-MPL-RabbitMQ @@ -0,0 +1,373 @@ +Mozilla Public License Version 2.0 +================================== + +1. Definitions +-------------- + +1.1. "Contributor" + means each individual or legal entity that creates, contributes to + the creation of, or owns Covered Software. + +1.2. "Contributor Version" + means the combination of the Contributions of others (if any) used + by a Contributor and that particular Contributor's Contribution. + +1.3. "Contribution" + means Covered Software of a particular Contributor. + +1.4. "Covered Software" + means Source Code Form to which the initial Contributor has attached + the notice in Exhibit A, the Executable Form of such Source Code + Form, and Modifications of such Source Code Form, in each case + including portions thereof. + +1.5. "Incompatible With Secondary Licenses" + means + + (a) that the initial Contributor has attached the notice described + in Exhibit B to the Covered Software; or + + (b) that the Covered Software was made available under the terms of + version 1.1 or earlier of the License, but not also under the + terms of a Secondary License. + +1.6. "Executable Form" + means any form of the work other than Source Code Form. + +1.7. "Larger Work" + means a work that combines Covered Software with other material, in + a separate file or files, that is not Covered Software. + +1.8. "License" + means this document. + +1.9. "Licensable" + means having the right to grant, to the maximum extent possible, + whether at the time of the initial grant or subsequently, any and + all of the rights conveyed by this License. + +1.10. "Modifications" + means any of the following: + + (a) any file in Source Code Form that results from an addition to, + deletion from, or modification of the contents of Covered + Software; or + + (b) any new file in Source Code Form that contains any Covered + Software. + +1.11. "Patent Claims" of a Contributor + means any patent claim(s), including without limitation, method, + process, and apparatus claims, in any patent Licensable by such + Contributor that would be infringed, but for the grant of the + License, by the making, using, selling, offering for sale, having + made, import, or transfer of either its Contributions or its + Contributor Version. + +1.12. "Secondary License" + means either the GNU General Public License, Version 2.0, the GNU + Lesser General Public License, Version 2.1, the GNU Affero General + Public License, Version 3.0, or any later versions of those + licenses. + +1.13. "Source Code Form" + means the form of the work preferred for making modifications. + +1.14. "You" (or "Your") + means an individual or a legal entity exercising rights under this + License. For legal entities, "You" includes any entity that + controls, is controlled by, or is under common control with You. For + purposes of this definition, "control" means (a) the power, direct + or indirect, to cause the direction or management of such entity, + whether by contract or otherwise, or (b) ownership of more than + fifty percent (50%) of the outstanding shares or beneficial + ownership of such entity. + +2. License Grants and Conditions +-------------------------------- + +2.1. Grants + +Each Contributor hereby grants You a world-wide, royalty-free, +non-exclusive license: + +(a) under intellectual property rights (other than patent or trademark) + Licensable by such Contributor to use, reproduce, make available, + modify, display, perform, distribute, and otherwise exploit its + Contributions, either on an unmodified basis, with Modifications, or + as part of a Larger Work; and + +(b) under Patent Claims of such Contributor to make, use, sell, offer + for sale, have made, import, and otherwise transfer either its + Contributions or its Contributor Version. + +2.2. Effective Date + +The licenses granted in Section 2.1 with respect to any Contribution +become effective for each Contribution on the date the Contributor first +distributes such Contribution. + +2.3. Limitations on Grant Scope + +The licenses granted in this Section 2 are the only rights granted under +this License. No additional rights or licenses will be implied from the +distribution or licensing of Covered Software under this License. +Notwithstanding Section 2.1(b) above, no patent license is granted by a +Contributor: + +(a) for any code that a Contributor has removed from Covered Software; + or + +(b) for infringements caused by: (i) Your and any other third party's + modifications of Covered Software, or (ii) the combination of its + Contributions with other software (except as part of its Contributor + Version); or + +(c) under Patent Claims infringed by Covered Software in the absence of + its Contributions. + +This License does not grant any rights in the trademarks, service marks, +or logos of any Contributor (except as may be necessary to comply with +the notice requirements in Section 3.4). + +2.4. Subsequent Licenses + +No Contributor makes additional grants as a result of Your choice to +distribute the Covered Software under a subsequent version of this +License (see Section 10.2) or under the terms of a Secondary License (if +permitted under the terms of Section 3.3). + +2.5. Representation + +Each Contributor represents that the Contributor believes its +Contributions are its original creation(s) or it has sufficient rights +to grant the rights to its Contributions conveyed by this License. + +2.6. Fair Use + +This License is not intended to limit any rights You have under +applicable copyright doctrines of fair use, fair dealing, or other +equivalents. + +2.7. Conditions + +Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted +in Section 2.1. + +3. Responsibilities +------------------- + +3.1. Distribution of Source Form + +All distribution of Covered Software in Source Code Form, including any +Modifications that You create or to which You contribute, must be under +the terms of this License. You must inform recipients that the Source +Code Form of the Covered Software is governed by the terms of this +License, and how they can obtain a copy of this License. You may not +attempt to alter or restrict the recipients' rights in the Source Code +Form. + +3.2. Distribution of Executable Form + +If You distribute Covered Software in Executable Form then: + +(a) such Covered Software must also be made available in Source Code + Form, as described in Section 3.1, and You must inform recipients of + the Executable Form how they can obtain a copy of such Source Code + Form by reasonable means in a timely manner, at a charge no more + than the cost of distribution to the recipient; and + +(b) You may distribute such Executable Form under the terms of this + License, or sublicense it under different terms, provided that the + license for the Executable Form does not attempt to limit or alter + the recipients' rights in the Source Code Form under this License. + +3.3. Distribution of a Larger Work + +You may create and distribute a Larger Work under terms of Your choice, +provided that You also comply with the requirements of this License for +the Covered Software. If the Larger Work is a combination of Covered +Software with a work governed by one or more Secondary Licenses, and the +Covered Software is not Incompatible With Secondary Licenses, this +License permits You to additionally distribute such Covered Software +under the terms of such Secondary License(s), so that the recipient of +the Larger Work may, at their option, further distribute the Covered +Software under the terms of either this License or such Secondary +License(s). + +3.4. Notices + +You may not remove or alter the substance of any license notices +(including copyright notices, patent notices, disclaimers of warranty, +or limitations of liability) contained within the Source Code Form of +the Covered Software, except that You may alter any license notices to +the extent required to remedy known factual inaccuracies. + +3.5. Application of Additional Terms + +You may choose to offer, and to charge a fee for, warranty, support, +indemnity or liability obligations to one or more recipients of Covered +Software. However, You may do so only on Your own behalf, and not on +behalf of any Contributor. You must make it absolutely clear that any +such warranty, support, indemnity, or liability obligation is offered by +You alone, and You hereby agree to indemnify every Contributor for any +liability incurred by such Contributor as a result of warranty, support, +indemnity or liability terms You offer. You may include additional +disclaimers of warranty and limitations of liability specific to any +jurisdiction. + +4. Inability to Comply Due to Statute or Regulation +--------------------------------------------------- + +If it is impossible for You to comply with any of the terms of this +License with respect to some or all of the Covered Software due to +statute, judicial order, or regulation then You must: (a) comply with +the terms of this License to the maximum extent possible; and (b) +describe the limitations and the code they affect. Such description must +be placed in a text file included with all distributions of the Covered +Software under this License. Except to the extent prohibited by statute +or regulation, such description must be sufficiently detailed for a +recipient of ordinary skill to be able to understand it. + +5. Termination +-------------- + +5.1. The rights granted under this License will terminate automatically +if You fail to comply with any of its terms. However, if You become +compliant, then the rights granted under this License from a particular +Contributor are reinstated (a) provisionally, unless and until such +Contributor explicitly and finally terminates Your grants, and (b) on an +ongoing basis, if such Contributor fails to notify You of the +non-compliance by some reasonable means prior to 60 days after You have +come back into compliance. Moreover, Your grants from a particular +Contributor are reinstated on an ongoing basis if such Contributor +notifies You of the non-compliance by some reasonable means, this is the +first time You have received notice of non-compliance with this License +from such Contributor, and You become compliant prior to 30 days after +Your receipt of the notice. + +5.2. If You initiate litigation against any entity by asserting a patent +infringement claim (excluding declaratory judgment actions, +counter-claims, and cross-claims) alleging that a Contributor Version +directly or indirectly infringes any patent, then the rights granted to +You by any and all Contributors for the Covered Software under Section +2.1 of this License shall terminate. + +5.3. In the event of termination under Sections 5.1 or 5.2 above, all +end user license agreements (excluding distributors and resellers) which +have been validly granted by You or Your distributors under this License +prior to termination shall survive termination. + +************************************************************************ +* * +* 6. Disclaimer of Warranty * +* ------------------------- * +* * +* Covered Software is provided under this License on an "as is" * +* basis, without warranty of any kind, either expressed, implied, or * +* statutory, including, without limitation, warranties that the * +* Covered Software is free of defects, merchantable, fit for a * +* particular purpose or non-infringing. The entire risk as to the * +* quality and performance of the Covered Software is with You. * +* Should any Covered Software prove defective in any respect, You * +* (not any Contributor) assume the cost of any necessary servicing, * +* repair, or correction. This disclaimer of warranty constitutes an * +* essential part of this License. No use of any Covered Software is * +* authorized under this License except under this disclaimer. * +* * +************************************************************************ + +************************************************************************ +* * +* 7. Limitation of Liability * +* -------------------------- * +* * +* Under no circumstances and under no legal theory, whether tort * +* (including negligence), contract, or otherwise, shall any * +* Contributor, or anyone who distributes Covered Software as * +* permitted above, be liable to You for any direct, indirect, * +* special, incidental, or consequential damages of any character * +* including, without limitation, damages for lost profits, loss of * +* goodwill, work stoppage, computer failure or malfunction, or any * +* and all other commercial damages or losses, even if such party * +* shall have been informed of the possibility of such damages. This * +* limitation of liability shall not apply to liability for death or * +* personal injury resulting from such party's negligence to the * +* extent applicable law prohibits such limitation. Some * +* jurisdictions do not allow the exclusion or limitation of * +* incidental or consequential damages, so this exclusion and * +* limitation may not apply to You. * +* * +************************************************************************ + +8. Litigation +------------- + +Any litigation relating to this License may be brought only in the +courts of a jurisdiction where the defendant maintains its principal +place of business and such litigation shall be governed by laws of that +jurisdiction, without reference to its conflict-of-law provisions. +Nothing in this Section shall prevent a party's ability to bring +cross-claims or counter-claims. + +9. Miscellaneous +---------------- + +This License represents the complete agreement concerning the subject +matter hereof. If any provision of this License is held to be +unenforceable, such provision shall be reformed only to the extent +necessary to make it enforceable. Any law or regulation which provides +that the language of a contract shall be construed against the drafter +shall not be used to construe this License against a Contributor. + +10. Versions of the License +--------------------------- + +10.1. New Versions + +Mozilla Foundation is the license steward. Except as provided in Section +10.3, no one other than the license steward has the right to modify or +publish new versions of this License. Each version will be given a +distinguishing version number. + +10.2. Effect of New Versions + +You may distribute the Covered Software under the terms of the version +of the License under which You originally received the Covered Software, +or under the terms of any subsequent version published by the license +steward. + +10.3. Modified Versions + +If you create software not governed by this License, and you want to +create a new license for such software, you may create and use a +modified version of this License if you rename the license and remove +any references to the name of the license steward (except to note that +such modified license differs from this License). + +10.4. Distributing Source Code Form that is Incompatible With Secondary +Licenses + +If You choose to distribute Source Code Form that is Incompatible With +Secondary Licenses under the terms of this version of the License, the +notice described in Exhibit B of this License must be attached. + +Exhibit A - Source Code Form License Notice +------------------------------------------- + + This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at http://mozilla.org/MPL/2.0/. + +If it is not possible or desirable to put the notice in a particular +file, then You may include the notice in a location (such as a LICENSE +file in a relevant directory) where a recipient would be likely to look +for such a notice. + +You may add additional accurate notices of copyright ownership. + +Exhibit B - "Incompatible With Secondary Licenses" Notice +--------------------------------------------------------- + + This Source Code Form is "Incompatible With Secondary Licenses", as + defined by the Mozilla Public License, v. 2.0. diff --git a/deps/rabbitmq_amqp_client/Makefile b/deps/rabbitmq_amqp_client/Makefile new file mode 100644 index 000000000000..99ec4850555b --- /dev/null +++ b/deps/rabbitmq_amqp_client/Makefile @@ -0,0 +1,22 @@ +PROJECT = rabbitmq_amqp_client +PROJECT_DESCRIPTION = AMQP 1.0 client for RabbitMQ + +DEPS = amqp10_client +TEST_DEPS = rabbitmq_ct_helpers + +BUILD_DEPS = rabbit_common +DEP_EARLY_PLUGINS = rabbit_common/mk/rabbitmq-early-plugin.mk +TEST_DEPS = rabbit rabbitmq_ct_helpers + +DEP_PLUGINS = rabbit_common/mk/rabbitmq-macros.mk \ + rabbit_common/mk/rabbitmq-build.mk \ + rabbit_common/mk/rabbitmq-hexpm.mk \ + rabbit_common/mk/rabbitmq-dist.mk \ + rabbit_common/mk/rabbitmq-run.mk \ + rabbit_common/mk/rabbitmq-test.mk \ + rabbit_common/mk/rabbitmq-tools.mk + +.DEFAULT_GOAL = all + +include rabbitmq-components.mk +include erlang.mk diff --git a/deps/rabbitmq_amqp_client/README.md b/deps/rabbitmq_amqp_client/README.md new file mode 100644 index 000000000000..b19ab34c9412 --- /dev/null +++ b/deps/rabbitmq_amqp_client/README.md @@ -0,0 +1,29 @@ +# Erlang RabbitMQ AMQP 1.0 Client + +The [Erlang AMQP 1.0 client](../amqp10_client/) is a client that can communicate with any AMQP 1.0 broker. +In contrast, this project (Erlang **RabbitMQ** AMQP 1.0 Client) can only communicate with RabbitMQ. +This project wraps (i.e. depends on) the Erlang AMQP 1.0 client providing additionally the following RabbitMQ management operations: +* declare queue +* get queue +* delete queue +* purge queue +* bind queue to exchange +* unbind queue from exchange +* declare exchange +* delete exchange +* bind exchange to exchange +* unbind exchange from exchange + +Except for `get queue`, these management operations are defined in the [AMQP 0.9.1 protocol](https://www.rabbitmq.com/amqp-0-9-1-reference.html). +To support these AMQP 0.9.1 / RabbitMQ specific operations over AMQP 1.0, this project implements a subset of the following (most recent) AMQP 1.0 extension specifications: +* [AMQP Request-Response Messaging with Link Pairing Version 1.0 - Committee Specification 01](https://docs.oasis-open.org/amqp/linkpair/v1.0/cs01/linkpair-v1.0-cs01.html) (February 2021) +* [HTTP Semantics and Content over AMQP Version 1.0 - Working Draft 06](https://groups.oasis-open.org/higherlogic/ws/public/document?document_id=65571) (July 2019) +* [AMQP Management Version 1.0 - Working Draft 16](https://groups.oasis-open.org/higherlogic/ws/public/document?document_id=65575) (July 2019) + +This project might support more (non AMQP 0.9.1) RabbitMQ operations via AMQP 1.0 in the future. + +Topologies (exchanges, bindings, queues) in RabbitMQ can be created via +* [Management HTTP API](https://www.rabbitmq.com/docs/management#http-api) +* [Definition Import](https://www.rabbitmq.com/docs/definitions#import) +* AMQP 0.9.1 clients +* RabbitMQ AMQP 1.0 clients, such as this project diff --git a/deps/rabbitmq_amqp_client/app.bzl b/deps/rabbitmq_amqp_client/app.bzl new file mode 100644 index 000000000000..6f3e3c4c0446 --- /dev/null +++ b/deps/rabbitmq_amqp_client/app.bzl @@ -0,0 +1,73 @@ +load("@rules_erlang//:erlang_bytecode2.bzl", "erlang_bytecode") +load("@rules_erlang//:filegroup.bzl", "filegroup") + +def all_beam_files(name = "all_beam_files"): + filegroup( + name = "beam_files", + srcs = [":other_beam"], + ) + erlang_bytecode( + name = "other_beam", + srcs = ["src/rabbitmq_amqp_client.erl"], + hdrs = [":public_and_private_hdrs"], + app_name = "rabbitmq_amqp_client", + dest = "ebin", + erlc_opts = "//:erlc_opts", + deps = ["//deps/amqp10_common:erlang_app"], + ) + +def all_srcs(name = "all_srcs"): + filegroup( + name = "srcs", + srcs = ["src/rabbitmq_amqp_client.erl"], + ) + filegroup(name = "private_hdrs") + filegroup( + name = "public_hdrs", + srcs = ["include/rabbitmq_amqp_client.hrl"], + ) + filegroup(name = "priv") + filegroup( + name = "license_files", + srcs = [ + "LICENSE", + "LICENSE-MPL-RabbitMQ", + ], + ) + filegroup( + name = "public_and_private_hdrs", + srcs = [":private_hdrs", ":public_hdrs"], + ) + filegroup( + name = "all_srcs", + srcs = [":public_and_private_hdrs", ":srcs"], + ) + +def all_test_beam_files(name = "all_test_beam_files"): + erlang_bytecode( + name = "test_other_beam", + testonly = True, + srcs = ["src/rabbitmq_amqp_client.erl"], + hdrs = [":public_and_private_hdrs"], + app_name = "rabbitmq_amqp_client", + dest = "test", + erlc_opts = "//:test_erlc_opts", + deps = ["//deps/amqp10_common:erlang_app"], + ) + filegroup( + name = "test_beam_files", + testonly = True, + srcs = [":test_other_beam"], + ) + +def test_suite_beam_files(name = "test_suite_beam_files"): + erlang_bytecode( + name = "management_SUITE_beam_files", + testonly = True, + srcs = ["test/management_SUITE.erl"], + outs = ["test/management_SUITE.beam"], + hdrs = ["include/rabbitmq_amqp_client.hrl"], + app_name = "rabbitmq_amqp_client", + erlc_opts = "//:test_erlc_opts", + deps = ["//deps/amqp10_common:erlang_app", "//deps/rabbit_common:erlang_app"], + ) diff --git a/deps/rabbitmq_amqp_client/erlang.mk b/deps/rabbitmq_amqp_client/erlang.mk new file mode 120000 index 000000000000..59af4a527a9d --- /dev/null +++ b/deps/rabbitmq_amqp_client/erlang.mk @@ -0,0 +1 @@ +../../erlang.mk \ No newline at end of file diff --git a/deps/rabbitmq_amqp_client/include/rabbitmq_amqp_client.hrl b/deps/rabbitmq_amqp_client/include/rabbitmq_amqp_client.hrl new file mode 100644 index 000000000000..58ba4dab1d8a --- /dev/null +++ b/deps/rabbitmq_amqp_client/include/rabbitmq_amqp_client.hrl @@ -0,0 +1,4 @@ +-record(link_pair, {session :: pid(), + outgoing_link :: amqp10_client:link_ref(), + incoming_link :: amqp10_client:link_ref()}). +-type link_pair() :: #link_pair{}. diff --git a/deps/rabbitmq_amqp_client/rabbitmq-components.mk b/deps/rabbitmq_amqp_client/rabbitmq-components.mk new file mode 120000 index 000000000000..43c0d3567154 --- /dev/null +++ b/deps/rabbitmq_amqp_client/rabbitmq-components.mk @@ -0,0 +1 @@ +../../rabbitmq-components.mk \ No newline at end of file diff --git a/deps/rabbitmq_amqp_client/src/rabbitmq_amqp_client.erl b/deps/rabbitmq_amqp_client/src/rabbitmq_amqp_client.erl new file mode 100644 index 000000000000..fc5da6c7b4e4 --- /dev/null +++ b/deps/rabbitmq_amqp_client/src/rabbitmq_amqp_client.erl @@ -0,0 +1,478 @@ +%% This Source Code Form is subject to the terms of the Mozilla Public +%% License, v. 2.0. If a copy of the MPL was not distributed with this +%% file, You can obtain one at https://mozilla.org/MPL/2.0/. +%% +%% Copyright (c) 2007-2024 Broadcom. All Rights Reserved. The term โ€œBroadcomโ€ refers to Broadcom Inc. and/or its subsidiaries. All rights reserved. + +-module(rabbitmq_amqp_client). + +-feature(maybe_expr, enable). + +-include("rabbitmq_amqp_client.hrl"). +-include_lib("amqp10_common/include/amqp10_framing.hrl"). + +-export[ + %% link pair operations + attach_management_link_pair_sync/2, + detach_management_link_pair_sync/1, + + %% queue operations + get_queue/2, + declare_queue/3, + bind_queue/5, + unbind_queue/5, + purge_queue/2, + delete_queue/2, + + %% exchange operations + declare_exchange/3, + bind_exchange/5, + unbind_exchange/5, + delete_exchange/2 + ]. + +-define(TIMEOUT, 20_000). +-define(MANAGEMENT_NODE_ADDRESS, <<"/management">>). + +-type arguments() :: #{binary() => {atom(), term()}}. + +-type queue_info() :: #{name := binary(), + vhost := binary(), + durable := boolean(), + exclusive := boolean(), + auto_delete := boolean(), + arguments := arguments(), + type := binary(), + message_count := non_neg_integer(), + consumer_count := non_neg_integer(), + replicas => [binary()], + leader => binary()}. + +-type queue_properties() :: #{name := binary(), + durable => boolean(), + exclusive => boolean(), + auto_delete => boolean(), + arguments => arguments()}. + +-type exchange_properties() :: #{name := binary(), + type => binary(), + durable => boolean(), + auto_delete => boolean(), + internal => boolean(), + arguments => arguments()}. + +-type amqp10_prim() :: amqp10_binary_generator:amqp10_prim(). + +-spec attach_management_link_pair_sync(pid(), binary()) -> + {ok, link_pair()} | {error, term()}. +attach_management_link_pair_sync(Session, Name) -> + Terminus = #{address => ?MANAGEMENT_NODE_ADDRESS, + durable => none}, + OutgoingAttachArgs = #{name => Name, + role => {sender, Terminus}, + snd_settle_mode => settled, + rcv_settle_mode => first, + properties => #{<<"paired">> => true}}, + IncomingAttachArgs = OutgoingAttachArgs#{role := {receiver, Terminus, self()}, + filter => #{}}, + maybe + {ok, OutgoingRef} ?= attach(Session, OutgoingAttachArgs), + {ok, IncomingRef} ?= attach(Session, IncomingAttachArgs), + ok ?= await_attached(OutgoingRef), + ok ?= await_attached(IncomingRef), + {ok, #link_pair{session = Session, + outgoing_link = OutgoingRef, + incoming_link = IncomingRef}} + end. + +-spec attach(pid(), amqp10_client:attach_args()) -> + {ok, amqp10_client:link_ref()} | {error, term()}. +attach(Session, AttachArgs) -> + try amqp10_client:attach_link(Session, AttachArgs) + catch exit:Reason -> + {error, Reason} + end. + +-spec await_attached(amqp10_client:link_ref()) -> + ok | {error, term()}. +await_attached(Ref) -> + receive + {amqp10_event, {link, Ref, attached}} -> + ok; + {amqp10_event, {link, Ref, {detached, Err}}} -> + {error, Err} + after ?TIMEOUT -> + {error, timeout} + end. + +-spec detach_management_link_pair_sync(link_pair()) -> + ok | {error, term()}. +detach_management_link_pair_sync( + #link_pair{outgoing_link = OutgoingLink, + incoming_link = IncomingLink}) -> + maybe + ok ?= detach(OutgoingLink), + ok ?= detach(IncomingLink), + ok ?= await_detached(OutgoingLink), + await_detached(IncomingLink) + end. + +-spec detach(amqp10_client:link_ref()) -> + ok | {error, term()}. +detach(Ref) -> + try amqp10_client:detach_link(Ref) + catch exit:Reason -> + {error, Reason} + end. + +-spec await_detached(amqp10_client:link_ref()) -> + ok | {error, term()}. +await_detached(Ref) -> + receive + {amqp10_event, {link, Ref, {detached, normal}}} -> + ok; + {amqp10_event, {link, Ref, {detached, Err}}} -> + {error, Err} + after ?TIMEOUT -> + {error, timeout} + end. + +-spec get_queue(link_pair(), binary()) -> + {ok, queue_info()} | {error, term()}. +get_queue(LinkPair, QueueName) -> + QNameQuoted = uri_string:quote(QueueName), + Props = #{subject => <<"GET">>, + to => <<"/queues/", QNameQuoted/binary>>}, + case request(LinkPair, Props, null) of + {ok, Resp} -> + case is_success(Resp) of + true -> get_queue_info(Resp); + false -> {error, Resp} + end; + Err -> + Err + end. + +-spec declare_queue(link_pair(), binary(), queue_properties()) -> + {ok, queue_info()} | {error, term()}. +declare_queue(LinkPair, QueueName, QueueProperties) -> + Body0 = maps:fold( + fun(durable, V, L) when is_boolean(V) -> + [{{utf8, <<"durable">>}, {boolean, V}} | L]; + (exclusive, V, L) when is_boolean(V) -> + [{{utf8, <<"exclusive">>}, {boolean, V}} | L]; + (auto_delete, V, L) when is_boolean(V) -> + [{{utf8, <<"auto_delete">>}, {boolean, V}} | L]; + (arguments, V, L) -> + Args = encode_arguments(V), + [{{utf8, <<"arguments">>}, Args} | L] + end, [], QueueProperties), + Body = {map, Body0}, + QNameQuoted = uri_string:quote(QueueName), + Props = #{subject => <<"PUT">>, + to => <<"/queues/", QNameQuoted/binary>>}, + + case request(LinkPair, Props, Body) of + {ok, Resp} -> + case is_success(Resp) of + true -> get_queue_info(Resp); + false -> {error, Resp} + end; + Err -> + Err + end. + +-spec bind_queue(link_pair(), binary(), binary(), binary(), #{binary() => amqp10_prim()}) -> + ok | {error, term()}. +bind_queue(LinkPair, QueueName, ExchangeName, BindingKey, BindingArguments) -> + bind(<<"destination_queue">>, LinkPair, QueueName, ExchangeName, BindingKey, BindingArguments). + +-spec bind_exchange(link_pair(), binary(), binary(), binary(), #{binary() => amqp10_prim()}) -> + ok | {error, term()}. +bind_exchange(LinkPair, Destination, Source, BindingKey, BindingArguments) -> + bind(<<"destination_exchange">>, LinkPair, Destination, Source, BindingKey, BindingArguments). + +-spec bind(binary(), link_pair(), binary(), binary(), binary(), #{binary() => amqp10_prim()}) -> + ok | {error, term()}. +bind(DestinationKind, LinkPair, Destination, Source, BindingKey, BindingArguments) -> + Args = encode_arguments(BindingArguments), + Body = {map, [ + {{utf8, <<"source">>}, {utf8, Source}}, + {{utf8, DestinationKind}, {utf8, Destination}}, + {{utf8, <<"binding_key">>}, {utf8, BindingKey}}, + {{utf8, <<"arguments">>}, Args} + ]}, + Props = #{subject => <<"POST">>, + to => <<"/bindings">>}, + + case request(LinkPair, Props, Body) of + {ok, Resp} -> + case is_success(Resp) of + true -> ok; + false -> {error, Resp} + end; + Err -> + Err + end. + +-spec unbind_queue(link_pair(), binary(), binary(), binary(), #{binary() => amqp10_prim()}) -> + ok | {error, term()}. +unbind_queue(LinkPair, QueueName, ExchangeName, BindingKey, BindingArguments) -> + unbind($q, LinkPair, QueueName, ExchangeName, BindingKey, BindingArguments). + +-spec unbind_exchange(link_pair(), binary(), binary(), binary(), #{binary() => amqp10_prim()}) -> + ok | {error, term()}. +unbind_exchange(LinkPair, DestinationExchange, SourceExchange, BindingKey, BindingArguments) -> + unbind($e, LinkPair, DestinationExchange, SourceExchange, BindingKey, BindingArguments). + +-spec unbind(byte(), link_pair(), binary(), binary(), binary(), #{binary() => amqp10_prim()}) -> + ok | {error, term()}. +unbind(DestinationChar, LinkPair, Destination, Source, BindingKey, BindingArguments) + when map_size(BindingArguments) =:= 0 -> + SrcQ = uri_string:quote(Source), + DstQ = uri_string:quote(Destination), + KeyQ = uri_string:quote(BindingKey), + Uri = <<"/bindings/src=", SrcQ/binary, + ";dst", DestinationChar, $=, DstQ/binary, + ";key=", KeyQ/binary, + ";args=">>, + delete_binding(LinkPair, Uri); +unbind(DestinationChar, LinkPair, Destination, Source, BindingKey, BindingArguments) -> + Path = <<"/bindings">>, + Query = uri_string:compose_query( + [{<<"src">>, Source}, + {<<"dst", DestinationChar>>, Destination}, + {<<"key">>, BindingKey}]), + Uri0 = uri_string:recompose(#{path => Path, + query => Query}), + Props = #{subject => <<"GET">>, + to => Uri0}, + + case request(LinkPair, Props, null) of + {ok, Resp} -> + case is_success(Resp) of + true -> + #'v1_0.amqp_value'{content = {list, Bindings}} = amqp10_msg:body(Resp), + case search_binding_uri(BindingArguments, Bindings) of + {ok, Uri} -> + delete_binding(LinkPair, Uri); + not_found -> + ok + end; + false -> + {error, Resp} + end; + Err -> + Err + end. + +search_binding_uri(_, []) -> + not_found; +search_binding_uri(BindingArguments, [{map, Binding} | Bindings]) -> + case maps:from_list(Binding) of + #{{utf8, <<"arguments">>} := {map, Args0}, + {utf8, <<"location">>} := {utf8, Uri}} -> + Args = lists:map(fun({{utf8, Key}, TypeVal}) -> + {Key, TypeVal} + end, Args0), + case maps:from_list(Args) =:= BindingArguments of + true -> + {ok, Uri}; + false -> + search_binding_uri(BindingArguments, Bindings) + end; + _ -> + search_binding_uri(BindingArguments, Bindings) + end. + +-spec delete_binding(link_pair(), binary()) -> + ok | {error, term()}. +delete_binding(LinkPair, BindingUri) -> + Props = #{subject => <<"DELETE">>, + to => BindingUri}, + case request(LinkPair, Props, null) of + {ok, Resp} -> + case is_success(Resp) of + true -> ok; + false -> {error, Resp} + end; + Err -> + Err + end. + +-spec delete_queue(link_pair(), binary()) -> + {ok, map()} | {error, term()}. +delete_queue(LinkPair, QueueName) -> + purge_or_delete_queue(LinkPair, QueueName, <<>>). + +-spec purge_queue(link_pair(), binary()) -> + {ok, map()} | {error, term()}. +purge_queue(LinkPair, QueueName) -> + purge_or_delete_queue(LinkPair, QueueName, <<"/messages">>). + +-spec purge_or_delete_queue(link_pair(), binary(), binary()) -> + {ok, map()} | {error, term()}. +purge_or_delete_queue(LinkPair, QueueName, PathSuffix) -> + QNameQuoted = uri_string:quote(QueueName), + HttpRequestTarget = <<"/queues/", QNameQuoted/binary, PathSuffix/binary>>, + Props = #{subject => <<"DELETE">>, + to => HttpRequestTarget}, + case request(LinkPair, Props, null) of + {ok, Resp} -> + case is_success(Resp) of + true -> + #'v1_0.amqp_value'{content = {map, KVList}} = amqp10_msg:body(Resp), + #{{utf8, <<"message_count">>} := {ulong, Count}} = maps:from_list(KVList), + {ok, #{message_count => Count}}; + false -> + {error, Resp} + end; + Err -> + Err + end. + +-spec declare_exchange(link_pair(), binary(), exchange_properties()) -> + ok | {error, term()}. +declare_exchange(LinkPair, ExchangeName, ExchangeProperties) -> + Body0 = maps:fold( + fun(type, V, L) when is_binary(V) -> + [{{utf8, <<"type">>}, {utf8, V}} | L]; + (durable, V, L) when is_boolean(V) -> + [{{utf8, <<"durable">>}, {boolean, V}} | L]; + (auto_delete, V, L) when is_boolean(V) -> + [{{utf8, <<"auto_delete">>}, {boolean, V}} | L]; + (internal, V, L) when is_boolean(V) -> + [{{utf8, <<"internal">>}, {boolean, V}} | L]; + (arguments, V, L) -> + Args = encode_arguments(V), + [{{utf8, <<"arguments">>}, Args} | L] + end, [], ExchangeProperties), + Body = {map, Body0}, + + XNameQuoted = uri_string:quote(ExchangeName), + Props = #{subject => <<"PUT">>, + to => <<"/exchanges/", XNameQuoted/binary>>}, + + case request(LinkPair, Props, Body) of + {ok, Resp} -> + case is_success(Resp) of + true -> ok; + false -> {error, Resp} + end; + Err -> + Err + end. + +-spec delete_exchange(link_pair(), binary()) -> + ok | {error, term()}. +delete_exchange(LinkPair, ExchangeName) -> + XNameQuoted = uri_string:quote(ExchangeName), + Props = #{subject => <<"DELETE">>, + to => <<"/exchanges/", XNameQuoted/binary>>}, + case request(LinkPair, Props, null) of + {ok, Resp} -> + case is_success(Resp) of + true -> ok; + false -> {error, Resp} + end; + Err -> + Err + end. + +-spec request(link_pair(), amqp10_msg:amqp10_properties(), amqp10_prim()) -> + {ok, Response :: amqp10_msg:amqp10_msg()} | {error, term()}. +request(#link_pair{session = Session, + outgoing_link = OutgoingLink, + incoming_link = IncomingLink}, Properties, Body) -> + MessageId = message_id(), + Properties1 = Properties#{message_id => {binary, MessageId}, + reply_to => <<"$me">>}, + Request = amqp10_msg:new(<<>>, #'v1_0.amqp_value'{content = Body}, true), + Request1 = amqp10_msg:set_properties(Properties1, Request), + ok = amqp10_client:flow_link_credit(IncomingLink, 1, never), + case amqp10_client:send_msg(OutgoingLink, Request1) of + ok -> + receive {amqp10_msg, IncomingLink, Response} -> + #{correlation_id := MessageId} = amqp10_msg:properties(Response), + {ok, Response}; + {amqp10_event, {session, Session, {ended, Reason}}} -> + {error, {session_ended, Reason}} + after ?TIMEOUT -> + {error, timeout} + end; + Err -> + Err + end. + +-spec get_queue_info(amqp10_msg:amqp10_msg()) -> + {ok, queue_info()}. +get_queue_info(Response) -> + #'v1_0.amqp_value'{content = {map, KVList}} = amqp10_msg:body(Response), + RespMap = maps:from_list(KVList), + + RequiredQInfo = [<<"name">>, + <<"vhost">>, + <<"durable">>, + <<"exclusive">>, + <<"auto_delete">>, + <<"type">>, + <<"message_count">>, + <<"consumer_count">>], + Map0 = lists:foldl(fun(Key, M) -> + {ok, TypeVal} = maps:find({utf8, Key}, RespMap), + M#{binary_to_atom(Key) => amqp10_client_types:unpack(TypeVal)} + end, #{}, RequiredQInfo), + + {ok, {map, ArgsKVList}} = maps:find({utf8, <<"arguments">>}, RespMap), + ArgsMap = lists:foldl(fun({{utf8, K}, TypeVal}, M) -> + M#{K => TypeVal} + end, #{}, ArgsKVList), + Map1 = Map0#{arguments => ArgsMap}, + + Map2 = case maps:find({utf8, <<"replicas">>}, RespMap) of + {ok, {array, utf8, Arr}} -> + Replicas = lists:map(fun({utf8, Replica}) -> + Replica + end, Arr), + Map1#{replicas => Replicas}; + error -> + Map1 + end, + + Map = case maps:find({utf8, <<"leader">>}, RespMap) of + {ok, {utf8, Leader}} -> + Map2#{leader => Leader}; + error -> + Map2 + end, + {ok, Map}. + +-spec encode_arguments(arguments()) -> + {map, list(tuple())}. +encode_arguments(Arguments) -> + KVList = maps:fold( + fun(Key, TaggedVal, L) + when is_binary(Key) -> + [{{utf8, Key}, TaggedVal} | L] + end, [], Arguments), + {map, KVList}. + +%% "The message producer is usually responsible for setting the message-id in +%% such a way that it is assured to be globally unique." [3.2.4] +-spec message_id() -> binary(). +message_id() -> + rand:bytes(8). + +%% All successful 2xx and redirection 3xx status codes are interpreted as success. +%% We don't hard code any specific status code for now as the returned status +%% codes from RabbitMQ are subject to change. +-spec is_success(amqp10_msg:amqp10_msg()) -> boolean(). +is_success(Response) -> + case amqp10_msg:properties(Response) of + #{subject := <>} + when C =:= $2 orelse + C =:= $3 -> + true; + _ -> + false + end. diff --git a/deps/rabbitmq_amqp_client/test/management_SUITE.erl b/deps/rabbitmq_amqp_client/test/management_SUITE.erl new file mode 100644 index 000000000000..ca387a53150e --- /dev/null +++ b/deps/rabbitmq_amqp_client/test/management_SUITE.erl @@ -0,0 +1,1087 @@ +%% This Source Code Form is subject to the terms of the Mozilla Public +%% License, v. 2.0. If a copy of the MPL was not distributed with this +%% file, You can obtain one at https://mozilla.org/MPL/2.0/. +%% +%% Copyright (c) 2007-2024 Broadcom. All Rights Reserved. The term โ€œBroadcomโ€ refers to Broadcom Inc. and/or its subsidiaries. All rights reserved. +%% + +-module(management_SUITE). + +-include_lib("common_test/include/ct.hrl"). +-include_lib("eunit/include/eunit.hrl"). + +-include_lib("rabbitmq_amqp_client.hrl"). +-include_lib("amqp10_common/include/amqp10_framing.hrl"). +-include_lib("rabbit_common/include/rabbit.hrl"). + +-compile([export_all, + nowarn_export_all]). + +-import(rabbit_ct_helpers, + [eventually/1, + eventually/3 + ]). + +-import(rabbit_ct_broker_helpers, + [rpc/4, + rpc/5, + get_node_config/3 + ]). + +-define(DEFAULT_EXCHANGE, <<>>). + +suite() -> + [{timetrap, {minutes, 3}}]. + + +all() -> + [{group, cluster_size_1}, + {group, cluster_size_3} + ]. + +groups() -> + [ + {cluster_size_1, [shuffle], + [all_management_operations, + queue_binding_args, + queue_defaults, + queue_properties, + exchange_defaults, + bad_uri, + bad_queue_property, + bad_exchange_property, + bad_exchange_type, + get_queue_not_found, + declare_queue_default_queue_type, + declare_queue_empty_name, + declare_queue_line_feed, + declare_queue_amq_prefix, + declare_queue_inequivalent_fields, + declare_queue_inequivalent_exclusive, + declare_queue_invalid_field, + declare_default_exchange, + declare_exchange_amq_prefix, + declare_exchange_line_feed, + declare_exchange_inequivalent_fields, + delete_default_exchange, + delete_exchange_amq_prefix, + delete_exchange_carriage_return, + bind_source_default_exchange, + bind_destination_default_exchange, + bind_source_line_feed, + bind_destination_line_feed, + bind_missing_queue, + unbind_bad_binding_path_segment, + exclusive_queue, + purge_stream, + pipeline, + multiple_link_pairs, + link_attach_order, + drain, + session_flow_control + ]}, + {cluster_size_3, [shuffle], + [classic_queue_stopped, + queue_topology + ]} + ]. + +init_per_suite(Config) -> + {ok, _} = application:ensure_all_started(amqp10_client), + rabbit_ct_helpers:log_environment(), + Config. + +end_per_suite(Config) -> + Config. + +init_per_group(Group, Config) -> + Nodes = case Group of + cluster_size_1 -> 1; + cluster_size_3 -> 3 + end, + Suffix = rabbit_ct_helpers:testcase_absname(Config, "", "-"), + Config1 = rabbit_ct_helpers:set_config( + Config, [{rmq_nodes_count, Nodes}, + {rmq_nodename_suffix, Suffix}]), + rabbit_ct_helpers:run_setup_steps( + Config1, + rabbit_ct_broker_helpers:setup_steps()). + +end_per_group(_, Config) -> + rabbit_ct_helpers:run_teardown_steps( + Config, + rabbit_ct_broker_helpers:teardown_steps()). + +init_per_testcase(Testcase, Config) -> + rabbit_ct_helpers:testcase_started(Config, Testcase). + +end_per_testcase(Testcase, Config) -> + %% Assert that every testcase cleaned up. + eventually(?_assertEqual([], rpc(Config, rabbit_amqqueue, list, []))), + rabbit_ct_helpers:testcase_finished(Config, Testcase). + +all_management_operations(Config) -> + NodeName = get_node_config(Config, 0, nodename), + Node = atom_to_binary(NodeName), + Init = {_, LinkPair = #link_pair{session = Session}} = init(Config), + + QName = <<"my ๐Ÿ‡"/utf8>>, + QProps = #{durable => true, + exclusive => false, + auto_delete => false, + arguments => #{<<"x-queue-type">> => {utf8, <<"quorum">>}}}, + {ok, QInfo} = rabbitmq_amqp_client:declare_queue(LinkPair, QName, QProps), + ?assertEqual( + #{name => QName, + vhost => <<"/">>, + durable => true, + exclusive => false, + auto_delete => false, + arguments => #{<<"x-queue-type">> => {utf8, <<"quorum">>}}, + type => <<"quorum">>, + message_count => 0, + consumer_count => 0, + leader => Node, + replicas => [Node]}, + QInfo), + + %% This operation should be idempotent. + %% Also, exactly the same queue infos should be returned. + ?assertEqual({ok, QInfo}, + rabbitmq_amqp_client:declare_queue(LinkPair, QName, QProps)), + + %% get_queue/2 should also return the exact the same queue infos. + ?assertEqual({ok, QInfo}, + rabbitmq_amqp_client:get_queue(LinkPair, QName)), + + [Q] = rpc(Config, rabbit_amqqueue, list, []), + ?assert(rpc(Config, amqqueue, is_durable, [Q])), + ?assertNot(rpc(Config, amqqueue, is_exclusive, [Q])), + ?assertNot(rpc(Config, amqqueue, is_auto_delete, [Q])), + ?assertEqual(rabbit_quorum_queue, rpc(Config, amqqueue, get_type, [Q])), + + TargetAddr1 = <<"/amq/queue/", QName/binary>>, + {ok, Sender1} = amqp10_client:attach_sender_link(Session, <<"sender 1">>, TargetAddr1), + ok = wait_for_credit(Sender1), + flush(credited), + DTag1 = <<"tag 1">>, + Msg1 = amqp10_msg:new(DTag1, <<"m1">>, false), + ok = amqp10_client:send_msg(Sender1, Msg1), + ok = wait_for_accepted(DTag1), + + RoutingKey1 = BindingKey1 = <<"๐Ÿ—๏ธ 1"/utf8>>, + SourceExchange = <<"amq.direct">>, + ?assertEqual(ok, rabbitmq_amqp_client:bind_queue(LinkPair, QName, SourceExchange, BindingKey1, #{})), + %% This operation should be idempotent. + ?assertEqual(ok, rabbitmq_amqp_client:bind_queue(LinkPair, QName, SourceExchange, BindingKey1, #{})), + TargetAddr2 = <<"/exchange/", SourceExchange/binary, "/", RoutingKey1/binary>>, + + {ok, Sender2} = amqp10_client:attach_sender_link(Session, <<"sender 2">>, TargetAddr2), + ok = wait_for_credit(Sender2), + flush(credited), + DTag2 = <<"tag 2">>, + Msg2 = amqp10_msg:new(DTag2, <<"m2">>, false), + ok = amqp10_client:send_msg(Sender2, Msg2), + ok = wait_for_accepted(DTag2), + + ?assertEqual(ok, rabbitmq_amqp_client:unbind_queue(LinkPair, QName, SourceExchange, BindingKey1, #{})), + ?assertEqual(ok, rabbitmq_amqp_client:unbind_queue(LinkPair, QName, SourceExchange, BindingKey1, #{})), + DTag3 = <<"tag 3">>, + ok = amqp10_client:send_msg(Sender2, amqp10_msg:new(DTag3, <<"not routed">>, false)), + ok = wait_for_settlement(DTag3, released), + + XName = <<"my fanout exchange ๐Ÿฅณ"/utf8>>, + XProps = #{type => <<"fanout">>, + durable => false, + auto_delete => true, + internal => false, + arguments => #{<<"x-๐Ÿ“ฅ"/utf8>> => {utf8, <<"๐Ÿ“ฎ"/utf8>>}}}, + ?assertEqual(ok, rabbitmq_amqp_client:declare_exchange(LinkPair, XName, XProps)), + ?assertEqual(ok, rabbitmq_amqp_client:declare_exchange(LinkPair, XName, XProps)), + + {ok, Exchange} = rpc(Config, rabbit_exchange, lookup, [rabbit_misc:r(<<"/">>, exchange, XName)]), + ?assertMatch(#exchange{type = fanout, + durable = false, + auto_delete = true, + internal = false, + arguments = [{<<"x-๐Ÿ“ฅ"/utf8>>, longstr, <<"๐Ÿ“ฎ"/utf8>>}]}, + Exchange), + + TargetAddr3 = <<"/exchange/", XName/binary>>, + SourceExchange = <<"amq.direct">>, + ?assertEqual(ok, rabbitmq_amqp_client:bind_queue(LinkPair, QName, XName, <<"ignored">>, #{})), + ?assertEqual(ok, rabbitmq_amqp_client:bind_queue(LinkPair, QName, XName, <<"ignored">>, #{})), + + {ok, Sender3} = amqp10_client:attach_sender_link(Session, <<"sender 3">>, TargetAddr3), + ok = wait_for_credit(Sender3), + flush(credited), + DTag4 = <<"tag 4">>, + Msg3 = amqp10_msg:new(DTag4, <<"m3">>, false), + ok = amqp10_client:send_msg(Sender3, Msg3), + ok = wait_for_accepted(DTag4), + + RoutingKey2 = BindingKey2 = <<"key 2">>, + BindingArgs = #{<<" ๐Ÿ˜ฌ "/utf8>> => {utf8, <<" ๐Ÿ˜ฌ "/utf8>>}}, + ?assertEqual(ok, rabbitmq_amqp_client:bind_exchange(LinkPair, XName, SourceExchange, BindingKey2, BindingArgs)), + ?assertEqual(ok, rabbitmq_amqp_client:bind_exchange(LinkPair, XName, SourceExchange, BindingKey2, BindingArgs)), + TargetAddr4 = <<"/exchange/", SourceExchange/binary, "/", RoutingKey2/binary>>, + + {ok, Sender4} = amqp10_client:attach_sender_link(Session, <<"sender 4">>, TargetAddr4), + ok = wait_for_credit(Sender4), + flush(credited), + DTag5 = <<"tag 5">>, + Msg4 = amqp10_msg:new(DTag5, <<"m4">>, false), + ok = amqp10_client:send_msg(Sender4, Msg4), + ok = wait_for_accepted(DTag5), + + ?assertEqual(ok, rabbitmq_amqp_client:unbind_exchange(LinkPair, XName, SourceExchange, BindingKey2, BindingArgs)), + ?assertEqual(ok, rabbitmq_amqp_client:unbind_exchange(LinkPair, XName, SourceExchange, BindingKey2, BindingArgs)), + DTag6 = <<"tag 6">>, + ok = amqp10_client:send_msg(Sender4, amqp10_msg:new(DTag6, <<"not routed">>, false)), + ok = wait_for_settlement(DTag6, released), + + ?assertEqual(ok, rabbitmq_amqp_client:delete_exchange(LinkPair, XName)), + ?assertEqual(ok, rabbitmq_amqp_client:delete_exchange(LinkPair, XName)), + %% When we publish the next message, we expect: + %% 1. that the message is released because the exchange doesn't exist anymore, and + DTag7 = <<"tag 7">>, + ok = amqp10_client:send_msg(Sender3, amqp10_msg:new(DTag7, <<"not routed">>, false)), + ok = wait_for_settlement(DTag7, released), + %% 2. that the server closes the link, i.e. sends us a DETACH frame. + ExpectedError = #'v1_0.error'{condition = ?V_1_0_AMQP_ERROR_RESOURCE_DELETED}, + receive {amqp10_event, {link, Sender3, {detached, ExpectedError}}} -> ok + after 5000 -> ct:fail({missing_event, ?LINE}) + end, + + ?assertEqual({ok, #{message_count => 4}}, + rabbitmq_amqp_client:purge_queue(LinkPair, QName)), + + ?assertEqual({ok, #{message_count => 0}}, + rabbitmq_amqp_client:delete_queue(LinkPair, QName)), + ?assertEqual({ok, #{message_count => 0}}, + rabbitmq_amqp_client:delete_queue(LinkPair, QName)), + + ok = cleanup(Init). + +queue_defaults(Config) -> + Init = {_, LinkPair} = init(Config), + QName = atom_to_binary(?FUNCTION_NAME), + {ok, _} = rabbitmq_amqp_client:declare_queue(LinkPair, QName, #{}), + [Q] = rpc(Config, rabbit_amqqueue, list, []), + ?assert(rpc(Config, amqqueue, is_durable, [Q])), + ?assertNot(rpc(Config, amqqueue, is_exclusive, [Q])), + ?assertNot(rpc(Config, amqqueue, is_auto_delete, [Q])), + ?assertEqual([], rpc(Config, amqqueue, get_arguments, [Q])), + + {ok, _} = rabbitmq_amqp_client:delete_queue(LinkPair, QName), + ok = cleanup(Init). + +queue_properties(Config) -> + Init = {_, LinkPair} = init(Config), + QName = atom_to_binary(?FUNCTION_NAME), + {ok, _} = rabbitmq_amqp_client:declare_queue(LinkPair, QName, #{durable => false, + exclusive => true, + auto_delete => true}), + [Q] = rpc(Config, rabbit_amqqueue, list, []), + ?assertNot(rpc(Config, amqqueue, is_durable, [Q])), + ?assert(rpc(Config, amqqueue, is_exclusive, [Q])), + ?assert(rpc(Config, amqqueue, is_auto_delete, [Q])), + + {ok, _} = rabbitmq_amqp_client:delete_queue(LinkPair, QName), + ok = cleanup(Init). + +exchange_defaults(Config) -> + Init = {_, LinkPair} = init(Config), + XName = atom_to_binary(?FUNCTION_NAME), + ok = rabbitmq_amqp_client:declare_exchange(LinkPair, XName, #{}), + {ok, Exchange} = rpc(Config, rabbit_exchange, lookup, [rabbit_misc:r(<<"/">>, exchange, XName)]), + ?assertMatch(#exchange{type = direct, + durable = true, + auto_delete = false, + internal = false, + arguments = []}, + Exchange), + + ok = rabbitmq_amqp_client:delete_exchange(LinkPair, XName), + ok = cleanup(Init). + +queue_binding_args(Config) -> + Init = {_, LinkPair = #link_pair{session = Session}} = init(Config), + QName = <<"my queue ~!@#$%^&*()_+๐Ÿ™ˆ`-=[]\;',./"/utf8>>, + Q = #{durable => false, + exclusive => true, + auto_delete => false, + arguments => #{<<"x-queue-type">> => {utf8, <<"classic">>}}}, + {ok, #{}} = rabbitmq_amqp_client:declare_queue(LinkPair, QName, Q), + + Exchange = <<"amq.headers">>, + BindingKey = <<>>, + BindingArgs = #{<<"key 1">> => {utf8, <<"๐Ÿ‘"/utf8>>}, + <<"key 2">> => {uint, 3}, + <<"key 3">> => true, + <<"x-match">> => {utf8, <<"all">>}}, + ?assertEqual(ok, rabbitmq_amqp_client:bind_queue(LinkPair, QName, Exchange, BindingKey, BindingArgs)), + + TargetAddr = <<"/exchange/amq.headers">>, + {ok, Sender} = amqp10_client:attach_sender_link(Session, <<"sender">>, TargetAddr), + ok = wait_for_credit(Sender), + flush(credited), + DTag1 = <<"tag 1">>, + Msg1 = amqp10_msg:new(DTag1, <<"m1">>, false), + AppProps = #{<<"key 1">> => <<"๐Ÿ‘"/utf8>>, + <<"key 2">> => 3, + <<"key 3">> => true}, + ok = amqp10_client:send_msg(Sender, amqp10_msg:set_application_properties(AppProps, Msg1)), + ok = wait_for_accepted(DTag1), + + DTag2 = <<"tag 2">>, + Msg2 = amqp10_msg:new(DTag2, <<"m2">>, false), + ok = amqp10_client:send_msg(Sender, + amqp10_msg:set_application_properties( + maps:remove(<<"key 2">>, AppProps), + Msg2)), + ok = wait_for_settlement(DTag2, released), + + ?assertEqual(ok, rabbitmq_amqp_client:unbind_queue(LinkPair, QName, Exchange, BindingKey, BindingArgs)), + + DTag3 = <<"tag 3">>, + Msg3 = amqp10_msg:new(DTag3, <<"m3">>, false), + ok = amqp10_client:send_msg(Sender, amqp10_msg:set_application_properties(AppProps, Msg3)), + ok = wait_for_settlement(DTag3, released), + + ?assertEqual({ok, #{message_count => 1}}, + rabbitmq_amqp_client:delete_queue(LinkPair, QName)), + + ok = amqp10_client:detach_link(Sender), + ok = cleanup(Init). + +bad_uri(Config) -> + Init = {_, #link_pair{outgoing_link = OutgoingLink, + incoming_link = IncomingLink}} = init(Config), + BadUri = <<"๐Ÿ‘Ž"/utf8>>, + Correlation = <<1, 2, 3>>, + Properties = #{subject => <<"GET">>, + to => BadUri, + message_id => {binary, Correlation}, + reply_to => <<"$me">>}, + Body = null, + Request0 = amqp10_msg:new(<<>>, #'v1_0.amqp_value'{content = Body}, true), + Request = amqp10_msg:set_properties(Properties, Request0), + ok = amqp10_client:flow_link_credit(IncomingLink, 1, never), + ok = amqp10_client:send_msg(OutgoingLink, Request), + + receive {amqp10_msg, IncomingLink, Response} -> + ?assertEqual( + #{subject => <<"400">>, + correlation_id => Correlation}, + amqp10_msg:properties(Response)), + ?assertEqual( + #'v1_0.amqp_value'{content = {utf8, <<"failed to normalize URI '๐Ÿ‘Ž': invalid_uri \"๐Ÿ‘Ž\""/utf8>>}}, + amqp10_msg:body(Response)) + after 5000 -> ct:fail({missing_message, ?LINE}) + end, + ok = cleanup(Init). + +bad_queue_property(Config) -> + bad_property(<<"queue">>, Config). + +bad_exchange_property(Config) -> + bad_property(<<"exchange">>, Config). + +bad_property(Kind, Config) -> + Init = {_, #link_pair{outgoing_link = OutgoingLink, + incoming_link = IncomingLink}} = init(Config), + Correlation = <<1>>, + Properties = #{subject => <<"PUT">>, + to => <<$/, Kind/binary, "s/my-object">>, + message_id => {binary, Correlation}, + reply_to => <<"$me">>}, + Body = {map, [{{utf8, <<"unknown">>}, {utf8, <<"bla">>}}]}, + Request0 = amqp10_msg:new(<<>>, #'v1_0.amqp_value'{content = Body}, true), + Request = amqp10_msg:set_properties(Properties, Request0), + ok = amqp10_client:flow_link_credit(IncomingLink, 1, never), + ok = amqp10_client:send_msg(OutgoingLink, Request), + + receive {amqp10_msg, IncomingLink, Response} -> + ?assertEqual( + #{subject => <<"400">>, + correlation_id => Correlation}, + amqp10_msg:properties(Response)), + ?assertEqual( + #'v1_0.amqp_value'{ + content = {utf8, <<"bad ", Kind/binary, " property {{utf8,<<\"unknown\">>},{utf8,<<\"bla\">>}}">>}}, + amqp10_msg:body(Response)) + after 5000 -> ct:fail({missing_message, ?LINE}) + end, + ok = cleanup(Init). + +bad_exchange_type(Config) -> + Init = {_, LinkPair} = init(Config), + UnknownXType = <<"๐Ÿคท"/utf8>>, + {error, Resp} = rabbitmq_amqp_client:declare_exchange(LinkPair, <<"e1">>, #{type => UnknownXType}), + ?assertMatch(#{subject := <<"400">>}, amqp10_msg:properties(Resp)), + ?assertEqual(#'v1_0.amqp_value'{content = {utf8, <<"unknown exchange type '", UnknownXType/binary, "'">>}}, + amqp10_msg:body(Resp)), + ok = cleanup(Init). + +get_queue_not_found(Config) -> + Init = {_, LinkPair} = init(Config), + QName = <<"๐Ÿคท"/utf8>>, + {error, Resp} = rabbitmq_amqp_client:get_queue(LinkPair, QName), + ?assertMatch(#{subject := <<"404">>}, amqp10_msg:properties(Resp)), + ?assertEqual(#'v1_0.amqp_value'{content = {utf8, <<"queue '", QName/binary, "' in vhost '/' not found">>}}, + amqp10_msg:body(Resp)), + ok = cleanup(Init). + +declare_queue_default_queue_type(Config) -> + Node = get_node_config(Config, 0, nodename), + Vhost = QName = atom_to_binary(?FUNCTION_NAME), + ok = erpc:call(Node, rabbit_vhost, add, + [Vhost, + #{default_queue_type => <<"quorum">>}, + <<"acting-user">>]), + ok = rabbit_ct_broker_helpers:set_full_permissions(Config, <<"guest">>, Vhost), + OpnConf = connection_config(Config, 0, Vhost), + {ok, Connection} = amqp10_client:open_connection(OpnConf), + {ok, Session} = amqp10_client:begin_session_sync(Connection), + {ok, LinkPair} = rabbitmq_amqp_client:attach_management_link_pair_sync(Session, <<"my link pair">>), + + ?assertMatch({ok, #{type := <<"quorum">>}}, + rabbitmq_amqp_client:declare_queue(LinkPair, QName, #{})), + + {ok, #{}} = rabbitmq_amqp_client:delete_queue(LinkPair, QName), + ok = rabbitmq_amqp_client:detach_management_link_pair_sync(LinkPair), + ok = amqp10_client:end_session(Session), + ok = amqp10_client:close_connection(Connection), + ok = rabbit_ct_broker_helpers:delete_vhost(Config, Vhost). + +declare_queue_empty_name(Config) -> + Init = {_, LinkPair} = init(Config), + QName = <<"">>, + {error, Resp} = rabbitmq_amqp_client:declare_queue(LinkPair, QName, #{}), + ?assertMatch(#{subject := <<"400">>}, amqp10_msg:properties(Resp)), + ?assertEqual(#'v1_0.amqp_value'{content = {utf8, <<"declare queue with empty name not allowed">>}}, + amqp10_msg:body(Resp)), + ok = cleanup(Init). + +declare_queue_line_feed(Config) -> + Init = {_, LinkPair} = init(Config), + QName = <<"๐Ÿค \n๐Ÿ˜ฑ"/utf8>>, + {error, Resp} = rabbitmq_amqp_client:declare_queue(LinkPair, QName, #{}), + ?assertMatch(#{subject := <<"400">>}, amqp10_msg:properties(Resp)), + ?assertEqual(#'v1_0.amqp_value'{ + content = {utf8, <<"Bad name '", QName/binary, + "': line feed and carriage return characters not allowed">>}}, + amqp10_msg:body(Resp)), + ok = cleanup(Init). + +declare_queue_amq_prefix(Config) -> + Init = {_, LinkPair} = init(Config), + QName = <<"amq.๐ŸŽ‡"/utf8>>, + {error, Resp} = rabbitmq_amqp_client:declare_queue(LinkPair, QName, #{}), + ?assertMatch(#{subject := <<"403">>}, amqp10_msg:properties(Resp)), + ?assertEqual(#'v1_0.amqp_value'{ + content = {utf8, <<"queue '", QName/binary, "' in vhost '/' " + "starts with reserved prefix 'amq.'">>}}, + amqp10_msg:body(Resp)), + ok = cleanup(Init). + +declare_queue_inequivalent_fields(Config) -> + Init = {_, LinkPair} = init(Config), + QName = <<"๐Ÿ‘Œ"/utf8>>, + {ok, #{auto_delete := false}} = rabbitmq_amqp_client:declare_queue(LinkPair, QName, #{auto_delete => false}), + {error, Resp} = rabbitmq_amqp_client:declare_queue(LinkPair, QName, #{auto_delete => true}), + ?assertMatch(#{subject := <<"409">>}, amqp10_msg:properties(Resp)), + ?assertEqual(#'v1_0.amqp_value'{ + content = {utf8, <<"inequivalent arg 'auto_delete' for queue '", QName/binary, + "' in vhost '/': received 'true' but current is 'false'">>}}, + amqp10_msg:body(Resp)), + {ok, #{}} = rabbitmq_amqp_client:delete_queue(LinkPair, QName), + ok = cleanup(Init). + +declare_queue_inequivalent_exclusive(Config) -> + Init = {_, LinkPair} = init(Config), + QName = <<"๐Ÿ‘Œ"/utf8>>, + {ok, #{exclusive := true}} = rabbitmq_amqp_client:declare_queue(LinkPair, QName, #{exclusive => true}), + {error, Resp} = rabbitmq_amqp_client:declare_queue(LinkPair, QName, #{exclusive => false}), + ?assertMatch(#{subject := <<"400">>}, amqp10_msg:properties(Resp)), + ?assertEqual( + #'v1_0.amqp_value'{ + content = {utf8, + <<"cannot obtain exclusive access to locked queue '", QName/binary, "' in vhost '/'. ", + "It could be originally declared on another connection or the exclusive property ", + "value does not match that of the original declaration.">>}}, + amqp10_msg:body(Resp)), + ok = cleanup(Init). + +declare_queue_invalid_field(Config) -> + Init = {_, LinkPair} = init(Config), + QName = <<"๐Ÿ‘Œ"/utf8>>, + QProps = #{auto_delete => true, + arguments => #{<<"x-queue-type">> => {utf8, <<"stream">>}}}, + {error, Resp} = rabbitmq_amqp_client:declare_queue(LinkPair, QName, QProps), + ?assertMatch(#{subject := <<"400">>}, amqp10_msg:properties(Resp)), + ?assertEqual( + #'v1_0.amqp_value'{ + content = {utf8, <<"invalid property 'auto-delete' for queue '", QName/binary, "' in vhost '/'">>}}, + amqp10_msg:body(Resp)), + ok = cleanup(Init). + +declare_default_exchange(Config) -> + Init = {_, LinkPair} = init(Config), + {error, Resp} = rabbitmq_amqp_client:declare_exchange(LinkPair, ?DEFAULT_EXCHANGE, #{}), + ?assertMatch(#{subject := <<"403">>}, amqp10_msg:properties(Resp)), + ?assertEqual(#'v1_0.amqp_value'{content = {utf8, <<"operation not permitted on the default exchange">>}}, + amqp10_msg:body(Resp)), + ok = cleanup(Init). + +declare_exchange_amq_prefix(Config) -> + Init = {_, LinkPair} = init(Config), + XName = <<"amq.๐ŸŽ‡"/utf8>>, + {error, Resp} = rabbitmq_amqp_client:declare_exchange(LinkPair, XName, #{}), + ?assertMatch(#{subject := <<"403">>}, amqp10_msg:properties(Resp)), + ?assertEqual(#'v1_0.amqp_value'{ + content = {utf8, <<"exchange '", XName/binary, "' in vhost '/' " + "starts with reserved prefix 'amq.'">>}}, + amqp10_msg:body(Resp)), + ok = cleanup(Init). + +declare_exchange_line_feed(Config) -> + Init = {_, LinkPair} = init(Config), + XName = <<"๐Ÿค \n๐Ÿ˜ฑ"/utf8>>, + {error, Resp} = rabbitmq_amqp_client:declare_exchange(LinkPair, XName, #{}), + ?assertMatch(#{subject := <<"400">>}, amqp10_msg:properties(Resp)), + ?assertEqual(#'v1_0.amqp_value'{ + content = {utf8, <<"Bad name '", XName/binary, + "': line feed and carriage return characters not allowed">>}}, + amqp10_msg:body(Resp)), + ok = cleanup(Init). + +declare_exchange_inequivalent_fields(Config) -> + Init = {_, LinkPair} = init(Config), + XName = <<"๐Ÿ‘Œ"/utf8>>, + ok = rabbitmq_amqp_client:declare_exchange(LinkPair, XName, #{type => <<"direct">>}), + {error, Resp} = rabbitmq_amqp_client:declare_exchange(LinkPair, XName, #{type => <<"fanout">>}), + ?assertMatch(#{subject := <<"409">>}, amqp10_msg:properties(Resp)), + ?assertEqual(#'v1_0.amqp_value'{ + content = {utf8, <<"inequivalent arg 'type' for exchange '", XName/binary, + "' in vhost '/': received 'fanout' but current is 'direct'">>}}, + amqp10_msg:body(Resp)), + ok = cleanup(Init). + +classic_queue_stopped(Config) -> + Init2 = {_, LinkPair2} = init(Config, 2), + QName = <<"๐Ÿ‘Œ"/utf8>>, + {ok, #{durable := true, + type := <<"classic">>}} = rabbitmq_amqp_client:declare_queue(LinkPair2, QName, #{}), + ok = cleanup(Init2), + ok = rabbit_ct_broker_helpers:stop_node(Config, 2), + %% Classic queue is now stopped. + + Init0 = {_, LinkPair0} = init(Config), + {error, Resp0} = rabbitmq_amqp_client:declare_queue(LinkPair0, QName, #{}), + ?assertMatch(#{subject := <<"400">>}, amqp10_msg:properties(Resp0)), + ExpectedResponseBody = #'v1_0.amqp_value'{ + content = {utf8, <<"queue '", QName/binary, + "' in vhost '/' process is stopped by supervisor">>}}, + ?assertEqual(ExpectedResponseBody, + amqp10_msg:body(Resp0)), + + {error, Resp1} = rabbitmq_amqp_client:get_queue(LinkPair0, QName), + ?assertMatch(#{subject := <<"400">>}, amqp10_msg:properties(Resp1)), + ?assertEqual(ExpectedResponseBody, + amqp10_msg:body(Resp1)), + + ok = rabbit_ct_broker_helpers:start_node(Config, 2), + {ok, #{}} = rabbitmq_amqp_client:delete_queue(LinkPair0, QName), + ok = cleanup(Init0). + +delete_default_exchange(Config) -> + Init = {_, LinkPair} = init(Config), + {error, Resp} = rabbitmq_amqp_client:delete_exchange(LinkPair, ?DEFAULT_EXCHANGE), + ?assertMatch(#{subject := <<"403">>}, amqp10_msg:properties(Resp)), + ?assertEqual(#'v1_0.amqp_value'{content = {utf8, <<"operation not permitted on the default exchange">>}}, + amqp10_msg:body(Resp)), + ok = cleanup(Init). + +delete_exchange_amq_prefix(Config) -> + Init = {_, LinkPair} = init(Config), + XName = <<"amq.fanout">>, + {error, Resp} = rabbitmq_amqp_client:delete_exchange(LinkPair, XName), + ?assertMatch(#{subject := <<"403">>}, amqp10_msg:properties(Resp)), + ?assertEqual(#'v1_0.amqp_value'{ + content = {utf8, <<"exchange '", XName/binary, "' in vhost '/' " + "starts with reserved prefix 'amq.'">>}}, + amqp10_msg:body(Resp)), + ok = cleanup(Init). + +delete_exchange_carriage_return(Config) -> + Init = {_, LinkPair} = init(Config), + XName = <<"x\rx">>, + {error, Resp} = rabbitmq_amqp_client:delete_exchange(LinkPair, XName), + ?assertMatch(#{subject := <<"400">>}, amqp10_msg:properties(Resp)), + ?assertEqual(#'v1_0.amqp_value'{ + content = {utf8, <<"Bad name '", XName/binary, + "': line feed and carriage return characters not allowed">>}}, + amqp10_msg:body(Resp)), + ok = cleanup(Init). + +bind_source_default_exchange(Config) -> + Init = {_, LinkPair} = init(Config), + QName = <<"๐Ÿ‘€"/utf8>>, + {ok, #{}} = rabbitmq_amqp_client:declare_queue(LinkPair, QName, #{}), + + {error, Resp} = rabbitmq_amqp_client:bind_queue( + LinkPair, QName, ?DEFAULT_EXCHANGE, <<"my binding key">>, #{}), + ?assertMatch(#{subject := <<"403">>}, amqp10_msg:properties(Resp)), + ?assertEqual(#'v1_0.amqp_value'{content = {utf8, <<"operation not permitted on the default exchange">>}}, + amqp10_msg:body(Resp)), + + {ok, #{}} = rabbitmq_amqp_client:delete_queue(LinkPair, QName), + ok = cleanup(Init). + +bind_destination_default_exchange(Config) -> + Init = {_, LinkPair} = init(Config), + {error, Resp} = rabbitmq_amqp_client:bind_exchange( + LinkPair, ?DEFAULT_EXCHANGE, <<"amq.fanout">>, <<"my binding key">>, #{}), + ?assertMatch(#{subject := <<"403">>}, amqp10_msg:properties(Resp)), + ?assertEqual(#'v1_0.amqp_value'{content = {utf8, <<"operation not permitted on the default exchange">>}}, + amqp10_msg:body(Resp)), + ok = cleanup(Init). + +bind_source_line_feed(Config) -> + Init = {_, LinkPair} = init(Config), + XName = <<"๐Ÿค \n๐Ÿ˜ฑ"/utf8>>, + {error, Resp} = rabbitmq_amqp_client:bind_exchange( + LinkPair, <<"amq.fanout">>, XName, <<"my binding key">>, #{}), + ?assertMatch(#{subject := <<"400">>}, amqp10_msg:properties(Resp)), + ?assertEqual(#'v1_0.amqp_value'{ + content = {utf8, <<"Bad name '", XName/binary, + "': line feed and carriage return characters not allowed">>}}, + amqp10_msg:body(Resp)), + ok = cleanup(Init). + +bind_destination_line_feed(Config) -> + Init = {_, LinkPair} = init(Config), + XName = <<"๐Ÿค \n๐Ÿ˜ฑ"/utf8>>, + {error, Resp} = rabbitmq_amqp_client:bind_exchange( + LinkPair, XName, <<"amq.fanout">>, <<"my binding key">>, #{}), + ?assertMatch(#{subject := <<"400">>}, amqp10_msg:properties(Resp)), + ?assertEqual(#'v1_0.amqp_value'{ + content = {utf8, <<"Bad name '", XName/binary, + "': line feed and carriage return characters not allowed">>}}, + amqp10_msg:body(Resp)), + ok = cleanup(Init). + +bind_missing_queue(Config) -> + Init = {_, LinkPair} = init(Config), + QName = <<"๐Ÿ‘€"/utf8>>, + {error, Resp} = rabbitmq_amqp_client:bind_queue( + LinkPair, QName, <<"amq.direct">>, <<"my binding key">>, #{}), + ?assertMatch(#{subject := <<"400">>}, amqp10_msg:properties(Resp)), + ?assertEqual(#'v1_0.amqp_value'{content = {utf8, <<"no queue '", QName/binary, "' in vhost '/'">>}}, + amqp10_msg:body(Resp)), + ok = cleanup(Init). + +unbind_bad_binding_path_segment(Config) -> + Init = {_, #link_pair{outgoing_link = OutgoingLink, + incoming_link = IncomingLink}} = init(Config), + Correlation = <<1>>, + BadBindingPathSegment = <<"src=e1;dstq=q1;invalidkey=k1;args=">>, + Properties = #{subject => <<"DELETE">>, + to => <<"/bindings/", BadBindingPathSegment/binary>>, + message_id => {binary, Correlation}, + reply_to => <<"$me">>}, + Request0 = amqp10_msg:new(<<>>, #'v1_0.amqp_value'{content = null}, true), + Request = amqp10_msg:set_properties(Properties, Request0), + ok = amqp10_client:flow_link_credit(IncomingLink, 1, never), + ok = amqp10_client:send_msg(OutgoingLink, Request), + receive {amqp10_msg, IncomingLink, Response} -> + ?assertEqual( + #{subject => <<"400">>, + correlation_id => Correlation}, + amqp10_msg:properties(Response)), + ?assertEqual( + #'v1_0.amqp_value'{ + content = {utf8, <<"bad binding path segment '", + BadBindingPathSegment/binary, "'">>}}, + amqp10_msg:body(Response)) + after 5000 -> ct:fail({missing_message, ?LINE}) + end, + ok = cleanup(Init). + +exclusive_queue(Config) -> + Init1 = {_, LinkPair1} = init(Config), + BindingKey = <<"๐Ÿ—๏ธ"/utf8>>, + XName = <<"amq.direct">>, + QName = <<"๐Ÿ™Œ"/utf8>>, + QProps = #{exclusive => true}, + {ok, #{}} = rabbitmq_amqp_client:declare_queue(LinkPair1, QName, QProps), + ok = rabbitmq_amqp_client:bind_queue(LinkPair1, QName, XName, BindingKey, #{}), + + {Conn2, LinkPair2} = init(Config), + {error, Resp1} = rabbitmq_amqp_client:bind_queue(LinkPair2, QName, XName, BindingKey, #{}), + ?assertMatch(#{subject := <<"400">>}, amqp10_msg:properties(Resp1)), + Body = #'v1_0.amqp_value'{content = {utf8, Reason}} = amqp10_msg:body(Resp1), + ?assertMatch(<<"cannot obtain exclusive access to locked queue '", + QName:(byte_size(QName))/binary, "' in vhost '/'.", _/binary >>, + Reason), + ok = amqp10_client:close_connection(Conn2), + + {Conn3, LinkPair3} = init(Config), + {error, Resp2} = rabbitmq_amqp_client:delete_queue(LinkPair3, QName), + ?assertMatch(#{subject := <<"400">>}, amqp10_msg:properties(Resp2)), + %% We expect the same error message as previously. + ?assertEqual(Body, amqp10_msg:body(Resp2)), + ok = amqp10_client:close_connection(Conn3), + + {Conn4, LinkPair4} = init(Config), + {error, Resp3} = rabbitmq_amqp_client:purge_queue(LinkPair4, QName), + ?assertMatch(#{subject := <<"400">>}, amqp10_msg:properties(Resp3)), + %% We expect the same error message as previously. + ?assertEqual(Body, amqp10_msg:body(Resp3)), + ok = amqp10_client:close_connection(Conn4), + + ok = rabbitmq_amqp_client:unbind_queue(LinkPair1, QName, XName, BindingKey, #{}), + {ok, #{}} = rabbitmq_amqp_client:delete_queue(LinkPair1, QName), + ok = cleanup(Init1). + +purge_stream(Config) -> + Init = {_, LinkPair} = init(Config), + QName = <<"๐Ÿš€"/utf8>>, + QProps = #{arguments => #{<<"x-queue-type">> => {utf8, <<"stream">>}}}, + {ok, #{}} = rabbitmq_amqp_client:declare_queue(LinkPair, QName, QProps), + + {error, Resp} = rabbitmq_amqp_client:purge_queue(LinkPair, QName), + ?assertMatch(#{subject := <<"400">>}, amqp10_msg:properties(Resp)), + #'v1_0.amqp_value'{content = {utf8, Reason}} = amqp10_msg:body(Resp), + ?assertEqual(<<"purge not supported by queue '", QName/binary, "' in vhost '/'">>, + Reason), + + {ok, #{}} = rabbitmq_amqp_client:delete_queue(LinkPair, QName), + ok = cleanup(Init). + +queue_topology(Config) -> + NodeNames = rabbit_ct_broker_helpers:get_node_configs(Config, nodename), + Nodes = [N0, N1, N2] = lists:map(fun erlang:atom_to_binary/1, NodeNames), + Init0 = {_, LinkPair0} = init(Config, 0), + + CQName = <<"my classic queue">>, + QQName = <<"my quorum queue">>, + SQName = <<"my stream queue">>, + + CQProps = #{arguments => #{<<"x-queue-type">> => {utf8, <<"classic">>}}}, + QQProps = #{arguments => #{<<"x-queue-type">> => {utf8, <<"quorum">>}}}, + SQProps = #{arguments => #{<<"x-queue-type">> => {utf8, <<"stream">>}}}, + + {ok, CQInfo0} = rabbitmq_amqp_client:declare_queue(LinkPair0, CQName, CQProps), + {ok, QQInfo0} = rabbitmq_amqp_client:declare_queue(LinkPair0, QQName, QQProps), + {ok, SQInfo0} = rabbitmq_amqp_client:declare_queue(LinkPair0, SQName, SQProps), + + %% The default queue leader strategy is client-local. + ?assertEqual({ok, N0}, maps:find(leader, CQInfo0)), + ?assertEqual({ok, N0}, maps:find(leader, QQInfo0)), + ?assertEqual({ok, N0}, maps:find(leader, SQInfo0)), + + ?assertEqual({ok, [N0]}, maps:find(replicas, CQInfo0)), + {ok, QQReplicas0} = maps:find(replicas, QQInfo0), + ?assertEqual(Nodes, lists:usort(QQReplicas0)), + {ok, SQReplicas0} = maps:find(replicas, SQInfo0), + ?assertEqual(Nodes, lists:usort(SQReplicas0)), + + ok = cleanup(Init0), + ok = rabbit_ct_broker_helpers:stop_node(Config, 0), + + Init2 = {_, LinkPair2} = init(Config, 2), + {ok, QQInfo2} = rabbitmq_amqp_client:get_queue(LinkPair2, QQName), + {ok, SQInfo2} = rabbitmq_amqp_client:get_queue(LinkPair2, SQName), + + case maps:get(leader, QQInfo2) of + N1 -> ok; + N2 -> ok; + Other0 -> ct:fail({?LINE, Other0}) + end, + case maps:get(leader, SQInfo2) of + N1 -> ok; + N2 -> ok; + Other1 -> ct:fail({?LINE, Other1}) + end, + + %% Replicas should include both online and offline replicas. + {ok, QQReplicas2} = maps:find(replicas, QQInfo2), + ?assertEqual(Nodes, lists:usort(QQReplicas2)), + {ok, SQReplicas2} = maps:find(replicas, SQInfo2), + ?assertEqual(Nodes, lists:usort(SQReplicas2)), + + ok = rabbit_ct_broker_helpers:start_node(Config, 0), + {ok, _} = rabbitmq_amqp_client:delete_queue(LinkPair2, CQName), + {ok, _} = rabbitmq_amqp_client:delete_queue(LinkPair2, QQName), + {ok, _} = rabbitmq_amqp_client:delete_queue(LinkPair2, SQName), + ok = cleanup(Init2). + +%% Even though RabbitMQ processes management requests synchronously (one at a time), +%% the client should be able to send multiple requests at once before receiving a response. +pipeline(Config) -> + Init = {_, LinkPair} = init(Config), + flush(attached), + + %% We should be able to send 8 management requests at once + %% because RabbitMQ grants us 8 link credits initially. + Num = 8, + pipeline0(Num, LinkPair, <<"PUT">>, {map, []}), + eventually(?_assertEqual(Num, rpc(Config, rabbit_amqqueue, count, [])), 200, 20), + flush(queues_created), + + pipeline0(Num, LinkPair, <<"DELETE">>, null), + eventually(?_assertEqual(0, rpc(Config, rabbit_amqqueue, count, [])), 200, 20), + flush(queues_deleted), + + ok = cleanup(Init). + +pipeline0(Num, + #link_pair{outgoing_link = OutgoingLink, + incoming_link = IncomingLink}, + HttpMethod, + Body) -> + ok = amqp10_client:flow_link_credit(IncomingLink, Num, never), + [begin + Request0 = amqp10_msg:new(<<>>, #'v1_0.amqp_value'{content = Body}, true), + Bin = integer_to_binary(N), + Props = #{subject => HttpMethod, + to => <<"/queues/q-", Bin/binary>>, + message_id => {binary, Bin}, + reply_to => <<"$me">>}, + Request = amqp10_msg:set_properties(Props, Request0), + ok = amqp10_client:send_msg(OutgoingLink, Request) + end || N <- lists:seq(1, Num)]. + +%% RabbitMQ allows attaching multiple link pairs. +multiple_link_pairs(Config) -> + OpnConf = connection_config(Config), + {ok, Connection} = amqp10_client:open_connection(OpnConf), + {ok, Session} = amqp10_client:begin_session_sync(Connection), + {ok, LinkPair1} = rabbitmq_amqp_client:attach_management_link_pair_sync(Session, <<"link pair 1">>), + {ok, LinkPair2} = rabbitmq_amqp_client:attach_management_link_pair_sync(Session, <<"link pair 2">>), + + [SessionPid] = rpc(Config, rabbit_amqp_session, list_local, []), + #{management_link_pairs := Pairs0, + incoming_management_links := Incoming0, + outgoing_management_links := Outgoing0} = gen_server_state(SessionPid), + ?assertEqual(2, maps:size(Pairs0)), + ?assertEqual(2, maps:size(Incoming0)), + ?assertEqual(2, maps:size(Outgoing0)), + + QName = <<"q">>, + {ok, #{}} = rabbitmq_amqp_client:declare_queue(LinkPair1, QName, #{}), + {ok, #{}} = rabbitmq_amqp_client:delete_queue(LinkPair2, QName), + + ok = rabbitmq_amqp_client:detach_management_link_pair_sync(LinkPair1), + ok = rabbitmq_amqp_client:detach_management_link_pair_sync(LinkPair2), + + %% Assert that the server cleaned up its state. + #{management_link_pairs := Pairs, + incoming_management_links := Incoming, + outgoing_management_links := Outgoing} = gen_server_state(SessionPid), + ?assertEqual(0, maps:size(Pairs)), + ?assertEqual(0, maps:size(Incoming)), + ?assertEqual(0, maps:size(Outgoing)), + + ok = amqp10_client:end_session(Session), + ok = amqp10_client:close_connection(Connection). + +%% Attaching (and detaching) either the sender or the receiver link first should both work. +link_attach_order(Config) -> + PairName1 = <<"link pair 1">>, + PairName2 = <<"link pair 2">>, + + OpnConf = connection_config(Config), + {ok, Connection} = amqp10_client:open_connection(OpnConf), + {ok, Session} = amqp10_client:begin_session_sync(Connection), + + Terminus = #{address => <<"/management">>, + durable => none}, + OutgoingAttachArgs1 = #{name => PairName1, + role => {sender, Terminus}, + snd_settle_mode => settled, + rcv_settle_mode => first, + properties => #{<<"paired">> => true}}, + IncomingAttachArgs1 = OutgoingAttachArgs1#{role := {receiver, Terminus, self()}, + filter => #{}}, + OutgoingAttachArgs2 = OutgoingAttachArgs1#{name := PairName2}, + IncomingAttachArgs2 = IncomingAttachArgs1#{name := PairName2}, + + %% Attach sender before receiver. + {ok, OutgoingRef1} = amqp10_client:attach_link(Session, OutgoingAttachArgs1), + {ok, IncomingRef1} = amqp10_client:attach_link(Session, IncomingAttachArgs1), + %% Attach receiver before sender. + {ok, IncomingRef2} = amqp10_client:attach_link(Session, IncomingAttachArgs2), + {ok, OutgoingRef2} = amqp10_client:attach_link(Session, OutgoingAttachArgs2), + + Refs = [OutgoingRef1, + OutgoingRef2, + IncomingRef1, + IncomingRef2], + + [ok = wait_for_event(Ref, attached) || Ref <- Refs], + flush(attached), + + LinkPair1 = #link_pair{session = Session, + outgoing_link = OutgoingRef1, + incoming_link = IncomingRef1}, + LinkPair2 = #link_pair{session = Session, + outgoing_link = OutgoingRef2, + incoming_link = IncomingRef2}, + + QName = <<"test queue">>, + {ok, #{}} = rabbitmq_amqp_client:declare_queue(LinkPair1, QName, #{}), + {ok, #{}} = rabbitmq_amqp_client:delete_queue(LinkPair2, QName), + + %% Detach sender before receiver. + ok = amqp10_client:detach_link(OutgoingRef1), + ok = amqp10_client:detach_link(IncomingRef1), + %% Detach receiver before sender. + ok = amqp10_client:detach_link(IncomingRef2), + ok = amqp10_client:detach_link(OutgoingRef2), + + [ok = wait_for_event(Ref, {detached, normal}) || Ref <- Refs], + flush(detached), + ok = amqp10_client:end_session(Session), + ok = amqp10_client:close_connection(Connection). + +drain(Config) -> + {Conn, #link_pair{session = Session, + outgoing_link = OutgoingLink, + incoming_link = IncomingLink}} = init(Config), + + ok = amqp10_client:flow_link_credit(IncomingLink, 2, never), + ok = amqp10_client:flow_link_credit(IncomingLink, 3, never, _Drain = true), + %% After draining, link credit on our incoming link should be 0. + + Request0 = amqp10_msg:new(<<>>, #'v1_0.amqp_value'{content = null}, true), + Props = #{subject => <<"DELETE">>, + to => <<"/queues/q1">>, + message_id => {binary, <<1>>}, + reply_to => <<"$me">>}, + Request = amqp10_msg:set_properties(Props, Request0), + ok = amqp10_client:send_msg(OutgoingLink, Request), + receive + {amqp10_event, + {session, Session, + {ended, + #'v1_0.error'{ + condition = ?V_1_0_AMQP_ERROR_PRECONDITION_FAILED, + description = {utf8, <<"insufficient credit (0) for management link from RabbitMQ to client">>}}}}} -> ok + after 5000 -> flush(missing_ended), + ct:fail({missing_event, ?LINE}) + end, + ok = amqp10_client:close_connection(Conn). + +%% Test that RabbitMQ respects session flow control. +session_flow_control(Config) -> + Init = {_, #link_pair{session = Session, + outgoing_link = OutgoingLink, + incoming_link = IncomingLink}} = init(Config), + flush(attached), + + ok = amqp10_client:flow_link_credit(IncomingLink, 1, never), + %% Close our incoming window. + gen_statem:cast(Session, {flow_session, #'v1_0.flow'{incoming_window = {uint, 0}}}), + + Request0 = amqp10_msg:new(<<>>, #'v1_0.amqp_value'{content = null}, true), + MessageId = <<1>>, + Props = #{subject => <<"DELETE">>, + to => <<"/queues/q1">>, + message_id => {binary, MessageId}, + reply_to => <<"$me">>}, + Request = amqp10_msg:set_properties(Props, Request0), + ok = amqp10_client:send_msg(OutgoingLink, Request), + + receive Unexpected -> ct:fail({unexpected, Unexpected}) + after 100 -> ok + end, + + %% Open our incoming window + gen_statem:cast(Session, {flow_session, #'v1_0.flow'{incoming_window = {uint, 5}}}), + + receive {amqp10_msg, IncomingLink, Response} -> + ?assertMatch(#{correlation_id := MessageId, + subject := <<"200">>}, + amqp10_msg:properties(Response)) + after 5000 -> flush(missing_msg), + ct:fail({missing_msg, ?LINE}) + end, + ok = cleanup(Init). + +init(Config) -> + init(Config, 0). + +init(Config, Node) -> + OpnConf = connection_config(Config, Node), + {ok, Connection} = amqp10_client:open_connection(OpnConf), + {ok, Session} = amqp10_client:begin_session_sync(Connection), + {ok, LinkPair} = rabbitmq_amqp_client:attach_management_link_pair_sync(Session, <<"my link pair">>), + {Connection, LinkPair}. + +cleanup({Connection, LinkPair = #link_pair{session = Session}}) -> + ok = rabbitmq_amqp_client:detach_management_link_pair_sync(LinkPair), + ok = amqp10_client:end_session(Session), + ok = amqp10_client:close_connection(Connection). + +connection_config(Config) -> + connection_config(Config, 0). + +connection_config(Config, Node) -> + connection_config(Config, Node, <<"/">>). + +connection_config(Config, Node, Vhost) -> + Host = ?config(rmq_hostname, Config), + Port = get_node_config(Config, Node, tcp_port_amqp), + #{address => Host, + port => Port, + container_id => <<"my container">>, + sasl => {plain, <<"guest">>, <<"guest">>}, + hostname => <<"vhost:", Vhost/binary>>}. + +wait_for_credit(Sender) -> + receive + {amqp10_event, {link, Sender, credited}} -> + ok + after 5000 -> + flush(?FUNCTION_NAME), + ct:fail(?FUNCTION_NAME) + end. + +flush(Prefix) -> + receive + Msg -> + ct:pal("~p flushed: ~p~n", [Prefix, Msg]), + flush(Prefix) + after 1 -> + ok + end. + +wait_for_accepted(Tag) -> + wait_for_settlement(Tag, accepted). + +wait_for_settlement(Tag, State) -> + receive + {amqp10_disposition, {State, Tag}} -> + ok + after 5000 -> + Reason = {?FUNCTION_NAME, Tag}, + flush(Reason), + ct:fail(Reason) + end. + +wait_for_event(Ref, Event) -> + receive {amqp10_event, {link, Ref, Event}} -> ok + after 5000 -> ct:fail({missing_event, Ref, Event}) + end. + +%% Return the formatted state of a gen_server via sys:get_status/1. +%% (sys:get_state/1 is unformatted) +gen_server_state(Pid) -> + {status, _, _, L0} = sys:get_status(Pid, 20_000), + L1 = lists:last(L0), + {data, L2} = lists:last(L1), + proplists:get_value("State", L2). diff --git a/deps/rabbitmq_management/test/rabbit_mgmt_http_SUITE.erl b/deps/rabbitmq_management/test/rabbit_mgmt_http_SUITE.erl index f64ad8cbf4d4..d8738b1de580 100644 --- a/deps/rabbitmq_management/test/rabbit_mgmt_http_SUITE.erl +++ b/deps/rabbitmq_management/test/rabbit_mgmt_http_SUITE.erl @@ -979,7 +979,7 @@ connections_test_amqp(Config) -> auth_mechanism := <<"PLAIN">>, protocol := <<"AMQP 1-0">>, client_properties := #{version := _, - product := <<"AMQP 1.0 client from the RabbitMQ Project">>, + product := <<"AMQP 1.0 client">>, platform := _}}, Connection1), ConnectionName = maps:get(name, Connection1), diff --git a/moduleindex.yaml b/moduleindex.yaml index 6b413170d6cf..3aca0fd97f16 100755 --- a/moduleindex.yaml +++ b/moduleindex.yaml @@ -544,6 +544,7 @@ rabbit: - rabbit_access_control - rabbit_alarm - rabbit_amqp1_0 +- rabbit_amqp_management - rabbit_amqp_reader - rabbit_amqp_session - rabbit_amqp_session_sup @@ -823,6 +824,8 @@ rabbit_common: - worker_pool - worker_pool_sup - worker_pool_worker +rabbitmq_amqp_client: +- rabbitmq_amqp_client rabbitmq_amqp1_0: - rabbitmq_amqp1_0_noop rabbitmq_auth_backend_cache: