diff --git a/deps/rabbit/src/rabbit_confirms.erl b/deps/rabbit/src/rabbit_confirms.erl index 2ea00bc9cb39..41a858f932f5 100644 --- a/deps/rabbit/src/rabbit_confirms.erl +++ b/deps/rabbit/src/rabbit_confirms.erl @@ -30,7 +30,8 @@ -opaque state() :: #?MODULE{}. -export_type([ - state/0 + state/0, + mx/0 ]). -spec init() -> state(). diff --git a/deps/rabbitmq_stomp/Makefile b/deps/rabbitmq_stomp/Makefile index a49e5e49c8c0..8bbf65e6dc81 100644 --- a/deps/rabbitmq_stomp/Makefile +++ b/deps/rabbitmq_stomp/Makefile @@ -9,7 +9,7 @@ define PROJECT_ENV {passcode, <<"guest">>}]}, {default_vhost, <<"/">>}, {default_topic_exchange, <<"amq.topic">>}, - {default_nack_requeue, true}, + {default_nack_requeue, true}, {ssl_cert_login, false}, {implicit_connect, false}, {tcp_listeners, [61613]}, diff --git a/deps/rabbitmq_stomp/include/rabbit_stomp.hrl b/deps/rabbitmq_stomp/include/rabbit_stomp.hrl index 99db62950d60..3d69c8ee0bc4 100644 --- a/deps/rabbitmq_stomp/include/rabbit_stomp.hrl +++ b/deps/rabbitmq_stomp/include/rabbit_stomp.hrl @@ -9,21 +9,28 @@ default_passcode, force_default_creds = false, implicit_connect, - ssl_cert_login}). + ssl_cert_login, + max_header_length, + max_headers, + max_body_length}). + -define(SUPPORTED_VERSIONS, ["1.0", "1.1", "1.2"]). + + -define(INFO_ITEMS, [conn_name, + name, + user, connection, connection_state, session_id, - channel, version, implicit_connect, auth_login, auth_mechanism, - peer_addr, + %% peer_addr, host, port, peer_host, @@ -42,3 +49,18 @@ -define(STOMP_GUIDE_URL, <<"https://rabbitmq.com/docs/stomp">>). -define(DEFAULT_MAX_FRAME_SIZE, 4 * 1024 * 1024). + +-define(SIMPLE_METRICS, + [pid, + recv_oct, + send_oct, + reductions]). +-define(OTHER_METRICS, + [recv_cnt, + send_cnt, + send_pend, + garbage_collection, + state, + timeout]). + +-type send_fun() :: fun ((async | sync, iodata()) -> ok | {atom(), any()}). diff --git a/deps/rabbitmq_stomp/include/rabbit_stomp_frame.hrl b/deps/rabbitmq_stomp/include/rabbit_stomp_frame.hrl index fb01ca8ec19c..2a4ec6f2691b 100644 --- a/deps/rabbitmq_stomp/include/rabbit_stomp_frame.hrl +++ b/deps/rabbitmq_stomp/include/rabbit_stomp_frame.hrl @@ -5,4 +5,9 @@ %% Copyright (c) 2007-2025 Broadcom. All Rights Reserved. The term “Broadcom” refers to Broadcom Inc. and/or its subsidiaries. All rights reserved. %% --record(stomp_frame, {command, headers, body_iolist}). +-record(stomp_frame, {command, headers, body_iolist_rev}). + +-record(stomp_parser_config, {max_header_length = 1024*100, + max_headers = 1000, + max_body_length = 1024*1024*100}). +-define(DEFAULT_STOMP_PARSER_CONFIG, #stomp_parser_config{}). diff --git a/deps/rabbitmq_stomp/include/rabbit_stomp_headers.hrl b/deps/rabbitmq_stomp/include/rabbit_stomp_headers.hrl index ed21c25831d3..31aa12f9bd0a 100644 --- a/deps/rabbitmq_stomp/include/rabbit_stomp_headers.hrl +++ b/deps/rabbitmq_stomp/include/rabbit_stomp_headers.hrl @@ -81,3 +81,20 @@ ?HEADER_EXCLUSIVE, ?HEADER_PERSISTENT ]). + + +-define(QUEUE_PREFIX, "/queue"). +-define(TOPIC_PREFIX, "/topic"). +-define(EXCHANGE_PREFIX, "/exchange"). +-define(AMQQUEUE_PREFIX, "/amq/queue"). +-define(TEMP_QUEUE_PREFIX, "/temp-queue"). +%% reply queues names can have slashes in the content so no further +%% parsing happens. +-define(REPLY_QUEUE_PREFIX, "/reply-queue/"). + +%%------------------------------------------------- + +-define(DEST_PREFIXES, [?EXCHANGE_PREFIX, ?TOPIC_PREFIX, ?QUEUE_PREFIX, + ?AMQQUEUE_PREFIX, ?REPLY_QUEUE_PREFIX]). + +-define(ALL_DEST_PREFIXES, [?TEMP_QUEUE_PREFIX | ?DEST_PREFIXES]). diff --git a/deps/rabbitmq_stomp/src/rabbit_stomp.erl b/deps/rabbitmq_stomp/src/rabbit_stomp.erl index 570df0f316c4..04be239625dd 100644 --- a/deps/rabbitmq_stomp/src/rabbit_stomp.erl +++ b/deps/rabbitmq_stomp/src/rabbit_stomp.erl @@ -20,14 +20,18 @@ -define(DEFAULT_CONFIGURATION, #stomp_configuration{ - default_login = undefined, - default_passcode = undefined, - implicit_connect = false, - ssl_cert_login = false}). + default_login = undefined, + default_passcode = undefined, + implicit_connect = false, + ssl_cert_login = false, + max_header_length = 1024*100, + max_headers = 1000, + max_body_length = 1024*1024*100}). start(normal, []) -> Config = parse_configuration(), Listeners = parse_listener_configuration(), + rabbit_global_counters:init([{protocol, stomp}]), Result = rabbit_stomp_sup:start_link(Listeners, Config), EMPid = case rabbit_event:start_link() of {ok, Pid} -> Pid; @@ -74,7 +78,11 @@ parse_configuration() -> {ok, SSLLogin} = application:get_env(ssl_cert_login), {ok, ImplicitConnect} = application:get_env(implicit_connect), Conf = Conf0#stomp_configuration{ssl_cert_login = SSLLogin, - implicit_connect = ImplicitConnect}, + implicit_connect = ImplicitConnect, + max_headers = application:get_env(max_headers, ?DEFAULT_CONFIGURATION#stomp_configuration.max_headers), + max_header_length = application:get_env(max_header_length, ?DEFAULT_CONFIGURATION#stomp_configuration.max_header_length), + max_body_length = application:get_env(max_body_length, ?DEFAULT_CONFIGURATION#stomp_configuration.max_body_length)}, + report_configuration(Conf), Conf. diff --git a/deps/rabbitmq_stomp/src/rabbit_stomp_frame.erl b/deps/rabbitmq_stomp/src/rabbit_stomp_frame.erl index da564d86eec3..442d0adb8eff 100644 --- a/deps/rabbitmq_stomp/src/rabbit_stomp_frame.erl +++ b/deps/rabbitmq_stomp/src/rabbit_stomp_frame.erl @@ -10,7 +10,7 @@ -include("rabbit_stomp_frame.hrl"). -include("rabbit_stomp_headers.hrl"). --export([parse/2, initial_state/0]). +-export([parse/2, initial_state/0, initial_state/1]). -export([header/2, header/3, boolean_header/2, boolean_header/3, integer_header/2, integer_header/3, @@ -18,7 +18,8 @@ -export([stream_offset_header/1, stream_filter_header/1]). -export([serialize/1, serialize/2]). -initial_state() -> none. +initial_state() -> {none, ?DEFAULT_STOMP_PARSER_CONFIG}. +initial_state(Config) -> {none, Config}. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %% STOMP 1.1 frames basic syntax @@ -73,11 +74,29 @@ initial_state() -> none. -define(COLON_ESC, $c). -define(CR_ESC, $r). +-define(COMMAND_TREE, + #{$S => #{$E => #{$N => #{$D => 'SEND'}}, + $U => #{$B => #{$S => #{$C => #{$R => #{$I => #{$B => #{$E => 'SUBSCRIBE'}}}}}}}, + $T => #{$O => #{$M => #{$P => 'STOMP'}}}}, + $U => #{$N => #{$S => #{$U => #{$B => #{$S => #{$C => #{$R => #{$I => #{$B => #{$E => 'UNSUBSCRIBE'}}}}}}}}}}, + $B => #{$E => #{$G => #{$I => #{$N => 'BEGIN'}}}}, + $C => #{$O => #{$M => #{$M => #{$I => #{$T => 'COMMIT'}}}, + $N => #{$N => #{$E => #{$C => #{$T => {'CONNECT', + #{$E => #{$D => 'CONNECTED'}}}}}}}}}, + $A => #{$B => #{$O => #{$R => #{$T => 'ABORT'}}}, + $C => #{$K => 'ACK'}}, + $N => #{$A => #{$C => #{$K => 'NACK'}}}, + $D => #{$I => #{$S => #{$C => #{$O => #{$N => #{$N => #{$E => #{$C => #{$T => 'DISCONNECT'}}}}}}}}}, + $M => #{$E => #{$S => #{$S => #{$A => #{$G => #{$E => 'MESSAGE'}}}}}}, + $R => #{$E => #{$C => #{$E => #{$I => #{$P => #{$T => 'RECEIPT'}}}}}}, + $E => #{$R => #{$R => #{$O => #{$R => 'ERROR'}}}}}). + %% parser state --record(state, {acc, cmd, hdrs, hdrname}). +-record(state, {acc, cmd, cmd_tree = ?COMMAND_TREE, hdrs, hdrname, hdrl = 0, + config}). parse(Content, {resume, Continuation}) -> Continuation(Content); -parse(Content, none ) -> parser(Content, noframe, #state{}). +parse(Content, {none, Config} ) -> parser(Content, noframe, #state{config=Config}). more(Continuation) -> {more, {resume, Continuation}}. @@ -103,14 +122,40 @@ parser( Rest, headers , State) -> goto(headers, hdrname, parser(<>, hdrname , State) -> goto(hdrname, hdrvalue, Rest, State); parser(<>, hdrname , State) -> goto(hdrname, headers, Rest, State); parser(<>, hdrvalue, State) -> goto(hdrvalue, headers, Rest, State); +parser(<>, command , #state{cmd_tree = {_, CmdTree}} = State) -> + case maps:get(Ch, CmdTree, undefined) of + undefined -> {error, unknown_command}; + NewCmdTree -> parser(Rest, command, State#state{cmd_tree = NewCmdTree}) + end; +parser(<>, command , #state{cmd_tree = #{} = CmdTree} = State) -> + case maps:get(Ch, CmdTree, undefined) of + undefined -> {error, unknown_command}; + NewCmdTree -> parser(Rest, command, State#state{cmd_tree = NewCmdTree}) + end; +parser(<<_Ch:8, _Rest/binary>>, command , _) -> + {error, unknown_command}; %% accumulate +%% parser(<>, Term = hdrname , State = #state{config = #stomp_parser_config{max_header_length = MaxHeaderLength}}) -> + +%% parser(Rest, Term, accum(Ch, State)); +%% parser(<>, Term = hdrvalue , State = #state{config = #stomp_parser_config{max_header_length = MaxHeaderLength}}) -> +%% parser(Rest, Term, accum(Ch, State)); parser(<>, Term , State) -> parser(Rest, Term, accum(Ch, State)). %% state transitions -goto(noframe, command, Rest, State ) -> parser(Rest, command, State#state{acc = []}); -goto(command, headers, Rest, State = #state{acc = Acc} ) -> parser(Rest, headers, State#state{cmd = lists:reverse(Acc), hdrs = []}); -goto(headers, body, Rest, #state{cmd = Cmd, hdrs = Hdrs}) -> parse_body(Rest, #stomp_frame{command = Cmd, headers = Hdrs}); -goto(headers, hdrname, Rest, State ) -> parser(Rest, hdrname, State#state{acc = []}); +goto(noframe, command, Rest, State ) -> parser(Rest, command, State#state{acc = [], cmd_tree = ?COMMAND_TREE}); +goto(command, headers, Rest, State = #state{cmd_tree = Command}) when is_atom(Command) -> + parser(Rest, headers, State#state{cmd = Command, hdrs = []}); +goto(command, headers, Rest, State = #state{cmd_tree = {Command, _}}) when is_atom(Command) -> + parser(Rest, headers, State#state{cmd = Command, hdrs = []}); +goto(command, headers, _Rest, _State)-> + {error, unknown_command}; +goto(headers, body, Rest, State ) -> parse_body(Rest, State); +goto(headers, hdrname, Rest, State = #state{hdrs = Headers, config = #stomp_parser_config{max_headers = MaxHeaders}}) -> + case length(Headers) == MaxHeaders of + true -> {error, {max_headeres, MaxHeaders}}; + _ -> parser(Rest, hdrname, State#state{acc = []}) + end; goto(hdrname, hdrvalue, Rest, State = #state{acc = Acc} ) -> parser(Rest, hdrvalue, State#state{acc = [], hdrname = lists:reverse(Acc)}); goto(hdrname, headers, _Rest, #state{acc = Acc} ) -> {error, {header_no_value, lists:reverse(Acc)}}; % badly formed header -- fatal error goto(hdrvalue, headers, Rest, State = #state{acc = Acc, hdrs = Headers, hdrname = HdrName}) -> @@ -140,31 +185,54 @@ insert_header(Headers, Name, Value) -> false -> [{Name, Value} | Headers] end. -parse_body(Content, Frame = #stomp_frame{command = Command}) -> - case Command of - "SEND" -> parse_body(Content, Frame, [], integer_header(Frame, ?HEADER_CONTENT_LENGTH, unknown)); - _ -> parse_body(Content, Frame, [], unknown) +parse_body(Content, State) -> + #state{cmd = Cmd, hdrs = Hdrs, config = #stomp_parser_config{max_body_length = MaxBodyLength}} = State, + Frame = #stomp_frame{command = Cmd, headers = Hdrs}, + case Cmd of + 'SEND' -> + case integer_header(Frame, ?HEADER_CONTENT_LENGTH, unknown) of + ContentLength when is_integer(ContentLength) and (ContentLength > MaxBodyLength) -> + {error, {max_body_length, ContentLength}}; + ContentLength when is_integer(ContentLength) -> + parse_known_body(Content, Frame, [], ContentLength); + _ -> + parse_unknown_body(Content, Frame, [], MaxBodyLength) + end; + _ -> + parse_unknown_body(Content, Frame, [], MaxBodyLength) end. -parse_body(Content, Frame, Chunks, unknown) -> - parse_body2(Content, Frame, Chunks, case firstnull(Content) of - -1 -> {more, unknown}; - Pos -> {done, Pos} - end); -parse_body(Content, Frame, Chunks, Remaining) -> +-define(MORE_BODY(Content, Frame, Chunks, Remaining), + Chunks1 = finalize_chunk(Content, Chunks), + more(fun(Rest) -> ?FUNCTION_NAME(Rest, Frame, Chunks1, Remaining) end)). + +parse_unknown_body(Content, Frame, Chunks, Remaining) -> + case firstnull(Content) of + -1 -> + ChunkSize = byte_size(Content), + case ChunkSize > Remaining of + true -> {error, {max_body_length, unknown}}; + false -> ?MORE_BODY(Content, Frame, Chunks, Remaining - ChunkSize) + end; + Pos -> + case Pos > Remaining of + true -> {error, {max_body_length, unknown}}; + false -> finish_body(Content, Frame, Chunks, Pos) + end + end. + +parse_known_body(Content, Frame, Chunks, Remaining) -> Size = byte_size(Content), - parse_body2(Content, Frame, Chunks, case Remaining >= Size of - true -> {more, Remaining - Size}; - false -> {done, Remaining} - end). - -parse_body2(Content, Frame, Chunks, {more, Left}) -> - Chunks1 = finalize_chunk(Content, Chunks), - more(fun(Rest) -> parse_body(Rest, Frame, Chunks1, Left) end); -parse_body2(Content, Frame, Chunks, {done, Pos}) -> + case Remaining >= Size of + true -> + ?MORE_BODY(Content, Frame, Chunks, Remaining - Size); + false -> finish_body(Content, Frame, Chunks, Remaining) + end. + +finish_body(Content, Frame, Chunks, Pos) -> <> = Content, - Body = lists:reverse(finalize_chunk(Chunk, Chunks)), - {ok, Frame#stomp_frame{body_iolist = Body}, Rest}. + Body = finalize_chunk(Chunk, Chunks), + {ok, Frame#stomp_frame{body_iolist_rev = Body}, Rest}. finalize_chunk(<<>>, Chunks) -> Chunks; finalize_chunk(Chunk, Chunks) -> [Chunk | Chunks]. @@ -249,16 +317,23 @@ serialize(Frame, true) -> serialize(Frame, false) ++ [?LF]; serialize(#stomp_frame{command = Command, headers = Headers, - body_iolist = BodyFragments}, false) -> + body_iolist_rev = BodyFragments}, false) -> Len = iolist_size(BodyFragments), - [Command, ?LF, + [serialize_command(Command), ?LF, lists:map(fun serialize_header/1, lists:keydelete(?HEADER_CONTENT_LENGTH, 1, Headers)), if Len > 0 -> [?HEADER_CONTENT_LENGTH ++ ":", integer_to_list(Len), ?LF]; true -> [] end, - ?LF, BodyFragments, 0]. + ?LF, case BodyFragments of + _ when is_binary(BodyFragments) -> BodyFragments; + _ -> lists:reverse(BodyFragments) + end, 0]. + +serialize_command(Command) when is_atom(Command) -> + atom_to_binary(Command, utf8); +serialize_command(Command) -> Command. serialize_header({K, V}) when is_integer(V) -> hdr(escape(K), integer_to_list(V)); serialize_header({K, V}) when is_boolean(V) -> hdr(escape(K), boolean_to_list(V)); diff --git a/deps/rabbitmq_stomp/src/rabbit_stomp_processor.erl b/deps/rabbitmq_stomp/src/rabbit_stomp_processor.erl index aeb694c395eb..4db0acc98f45 100644 --- a/deps/rabbitmq_stomp/src/rabbit_stomp_processor.erl +++ b/deps/rabbitmq_stomp/src/rabbit_stomp_processor.erl @@ -7,115 +7,199 @@ -module(rabbit_stomp_processor). +-feature(maybe_expr, enable). + -compile({no_auto_import, [error/3]}). --export([initial_state/2, process_frame/2, flush_and_die/1]). +-export([initial_state/2, + process_frame/2, + flush_and_die/1, + info/2]). + -export([flush_pending_receipts/3, - handle_exit/3, cancel_consumer/2, - send_delivery/5]). - --export([adapter_name/1]). --export([info/2]). + handle_down/2, + handle_queue_event/2]). --include_lib("amqp_client/include/amqp_client.hrl"). +-include_lib("kernel/include/logger.hrl"). -include("rabbit_stomp_frame.hrl"). -include("rabbit_stomp.hrl"). -include("rabbit_stomp_headers.hrl"). +-include_lib("rabbit/include/amqqueue.hrl"). +-include_lib("amqp_client/include/amqp_client.hrl"). --record(proc_state, {session_id, channel, connection, subscriptions, - version, start_heartbeat_fun, pending_receipts, - config, route_state, reply_queues, frame_transformer, - adapter_info, send_fun, ssl_login_name, peer_addr, - %% see rabbitmq/rabbitmq-stomp#39 - trailing_lf, auth_mechanism, auth_login, - default_topic_exchange, default_nack_requeue}). +-import(rabbit_misc, [maps_put_truthy/3]). + +-define(QUEUE, lqueue). +-define(MAX_PERMISSION_CACHE_SIZE, 12). +-record(subscription, {dest_hdr, ack_mode, multi_ack, description, queue_name}). +-type session_id() :: string(). +-type subscriptions() :: #{rabbit_types:ctag() => #subscription{}}. + +-type frame_transformer() :: fun ((#stomp_frame{}) -> #stomp_frame{}). + +-record(pending_ack, { + %% delivery identifier used by clients + %% to acknowledge and reject deliveries + delivery_tag :: non_neg_integer(), + %% consumer tag + tag :: rabbit_types:ctag(), + delivered_at :: integer(), + %% queue name + queue :: rabbit_amqqueue:name(), + %% message ID used by queue and message store implementations + msg_id :: rabbit_amqqueue:msg_id() + }). + +-record(cfg, + { + session_id :: session_id(), + version :: {1, 0 | 1 | 2}, + default_login :: undefined | binary(), + default_passcode :: undefined | binary(), + ssl_login_name :: none | binary(), + force_default_creds :: boolean(), + implicit_connect :: boolean(), + frame_transformer :: frame_transformer(), + adapter_info :: #amqp_adapter_info{}, + send_fun :: send_fun(), + peer_ip_addr :: inet:ip_address(), + trailing_lf :: boolean(), + auth_mechanism :: config | ssl | stomp_headers, + auth_login :: binary(), + vhost :: binary(), + default_topic_exchange :: binary(), + default_nack_requeue = true :: boolean(), + delivery_flow :: flow | noflow + }). + +-record(state, + { + cfg :: #cfg{}, + user :: #user{}, + authz_ctx :: #{}, + subscriptions :: subscriptions(), + pending_receipts :: gb_trees:tree(integer(), string()), + route_state :: sets:set(), + reply_queues :: #{string() => binary()}, + confirmed :: [rabbit_confirms:mx()], + rejected :: [rabbit_confirms:mx()], + unconfirmed :: rabbit_confirms:state(), + %% a map of queue names to consumer tag lists + queue_consumers :: #{rabbit_queue:name() => rabbit_types:ctag()}, + unacked_message_q :: ?QUEUE:?QUEUE(#pending_ack{}), + queue_states :: rabbit_queue_tyoe:state(), + delivery_tag = 0 :: non_neg_integer(), + msg_seq_no = 1 :: pos_integer() + }). --record(subscription, {dest_hdr, ack_mode, multi_ack, description}). +-type process_frame_result() :: + {ok, term(), #state{}} | + {stop, term(), #state{}}. --define(FLUSH_TIMEOUT, 60000). +-export_type ([process_frame_result/0]). -adapter_name(State) -> - #proc_state{adapter_info = #amqp_adapter_info{name = Name}} = State, - Name. +-export([adapter_name/1]). %%---------------------------------------------------------------------------- +%% Public API +%%---------------------------------------------------------------------------- +adapter_name(#state{cfg = #cfg{adapter_info = #amqp_adapter_info{name = Name}}}) -> + Name. -spec initial_state( #stomp_configuration{}, {SendFun, AdapterInfo, SSLLoginName, PeerAddr}) - -> #proc_state{} - when SendFun :: fun((binary()) -> term()), + -> #state{} + when SendFun :: send_fun(), AdapterInfo :: #amqp_adapter_info{}, - SSLLoginName :: atom() | binary(), + SSLLoginName :: none | binary(), PeerAddr :: inet:ip_address(). - --type process_frame_result() :: - {ok, term(), #proc_state{}} | - {stop, term(), #proc_state{}}. - --spec process_frame(#stomp_frame{}, #proc_state{}) -> - process_frame_result(). - --spec flush_and_die(#proc_state{}) -> #proc_state{}. - --spec command({Command, Frame}, State) -> process_frame_result() - when Command :: string(), - Frame :: #stomp_frame{}, - State :: #proc_state{}. - --type process_fun() :: fun((#proc_state{}) -> - {ok, #stomp_frame{}, #proc_state{}} | - {error, string(), string(), #proc_state{}} | - {stop, term(), #proc_state{}}). --spec process_request(process_fun(), fun((#proc_state{}) -> #proc_state{}), #proc_state{}) -> - process_frame_result(). - --spec flush_pending_receipts(DeliveryTag, IsMulti, State) -> State - when State :: #proc_state{}, - DeliveryTag :: term(), - IsMulti :: boolean(). - --spec handle_exit(From, Reason, State) -> unknown_exit | {stop, Reason, State} - when State :: #proc_state{}, - From :: pid(), - Reason :: term(). - --spec cancel_consumer(binary(), #proc_state{}) -> process_frame_result(). - --spec send_delivery(#'basic.deliver'{}, term(), term(), term(), - #proc_state{}) -> #proc_state{}. - -%%---------------------------------------------------------------------------- - - -%%---------------------------------------------------------------------------- -%% Public API -%%---------------------------------------------------------------------------- - +initial_state(Configuration, + {SendFun, AdapterInfo0 = #amqp_adapter_info{additional_info = Extra}, + SSLLoginName, PeerAddr}) -> + + %% STOMP connections use exactly one channel. The frame max is not + %% applicable and there is no way to know what client is used. + AdapterInfo = AdapterInfo0#amqp_adapter_info{additional_info=[ + {channels, 1}, + {channel_max, 1}, + {frame_max, 0}, + %% TODO: can we use a header to make it possible for clients + %% to override this value? + {client_properties, [{<<"product">>, longstr, <<"STOMP client">>}]} + |Extra]}, + + Flow = case rabbit_misc:get_env(rabbit, mirroring_flow_control, true) of + true -> flow; + false -> noflow + end, + #state { + cfg = #cfg{ + send_fun = SendFun, + adapter_info = AdapterInfo, + ssl_login_name = SSLLoginName, + peer_ip_addr = PeerAddr, + session_id = none, + frame_transformer = undefined, + version = none, + trailing_lf = application:get_env(rabbitmq_stomp, trailing_lf, true), + default_topic_exchange = application:get_env(rabbitmq_stomp, default_topic_exchange, <<"amq.topic">>), + default_nack_requeue = application:get_env(rabbitmq_stomp, default_nack_requeue, true), + default_login = Configuration#stomp_configuration.default_login, + default_passcode = Configuration#stomp_configuration.default_passcode, + force_default_creds = Configuration#stomp_configuration.force_default_creds, + delivery_flow = Flow + }, + subscriptions = #{}, + queue_consumers = #{}, + route_state = routing_init_state(), + reply_queues = #{}, + msg_seq_no = 1, + unconfirmed = rabbit_confirms:init(), + confirmed = [], + unacked_message_q = ?QUEUE:new(), + rejected = [], + queue_states = rabbit_queue_type:init(), + pending_receipts = gb_trees:empty() + }. + +-spec process_frame(#stomp_frame{}, #state{}) -> + process_frame_result(). process_frame(Frame = #stomp_frame{command = Command}, State) -> command({Command, Frame}, State). +-spec flush_and_die(#state{}) -> #state{}. flush_and_die(State) -> close_connection(State). -info(session_id, #proc_state{session_id = Val}) -> +-spec info(Key, State) -> Result + when + Key :: atom(), + State :: #state{}, + Result :: term(). %% TODO: somewhere these values are used to render things + %% to CLI and Management UI, what types do they support? +info(session_id, #state{cfg=#cfg{session_id = Val}}) -> Val; -info(channel, #proc_state{channel = Val}) -> Val; -info(version, #proc_state{version = Val}) -> Val; -info(implicit_connect, #proc_state{config = #stomp_configuration{implicit_connect = Val}}) -> Val; -info(auth_login, #proc_state{auth_login = Val}) -> Val; -info(auth_mechanism, #proc_state{auth_mechanism = Val}) -> Val; -info(peer_addr, #proc_state{peer_addr = Val}) -> Val; -info(host, #proc_state{adapter_info = #amqp_adapter_info{host = Val}}) -> Val; -info(port, #proc_state{adapter_info = #amqp_adapter_info{port = Val}}) -> Val; -info(peer_host, #proc_state{adapter_info = #amqp_adapter_info{peer_host = Val}}) -> Val; -info(peer_port, #proc_state{adapter_info = #amqp_adapter_info{peer_port = Val}}) -> Val; -info(protocol, #proc_state{adapter_info = #amqp_adapter_info{protocol = Val}}) -> - case Val of - {Proto, Version} -> {Proto, rabbit_data_coercion:to_binary(Version)}; - Other -> Other - end; +info(version, #state{cfg = #cfg{version = Val}}) -> Val; +info(implicit_connect, #state{cfg = #cfg{implicit_connect = Val}}) -> Val; +info(auth_login, #state{cfg = #cfg{auth_login = Val}}) -> Val; +info(auth_mechanism, #state{cfg = #cfg{auth_mechanism = Val}}) -> Val; +info(peer_addr, #state{cfg = #cfg{peer_ip_addr = Val}}) -> Val; +info(host, #state{cfg = #cfg{adapter_info = #amqp_adapter_info{host = Val}}}) -> Val; +info(port, #state{cfg = #cfg{adapter_info = #amqp_adapter_info{port = Val}}}) -> Val; +info(peer_host, #state{cfg = #cfg{adapter_info = #amqp_adapter_info{peer_host = Val}}}) -> Val; +info(peer_port, #state{cfg = #cfg{adapter_info = #amqp_adapter_info{peer_port = Val}}}) -> Val; +info(protocol, #state{cfg = #cfg{version = Version}}) -> + VersionTuple = case Version of + "1.0" -> {1, 0}; + "1.1" -> {1, 1}; + "1.2" -> {1, 2}; + _ -> none + end, + {'STOMP', VersionTuple}; +info(user, #state{user = undefined}) -> undefined; +info(user, #state{user = #user{username = Username}}) -> Username; info(channels, PState) -> additional_info(channels, PState); info(channel_max, PState) -> additional_info(channel_max, PState); info(frame_max, PState) -> additional_info(frame_max, PState); @@ -126,102 +210,67 @@ info(ssl_key_exchange, PState) -> additional_info(ssl_key_exchange, PState); info(ssl_cipher, PState) -> additional_info(ssl_cipher, PState); info(ssl_hash, PState) -> additional_info(ssl_hash, PState). -initial_state(Configuration, - {SendFun, AdapterInfo0 = #amqp_adapter_info{additional_info = Extra}, - SSLLoginName, PeerAddr}) -> - %% STOMP connections use exactly one channel. The frame max is not - %% applicable and there is no way to know what client is used. - AdapterInfo = AdapterInfo0#amqp_adapter_info{additional_info=[ - {channels, 1}, - {channel_max, 1}, - {frame_max, 0}, - %% TODO: can we use a header to make it possible for clients - %% to override this value? - {client_properties, [{<<"product">>, longstr, <<"STOMP client">>}]} - |Extra]}, - #proc_state { - send_fun = SendFun, - adapter_info = AdapterInfo, - ssl_login_name = SSLLoginName, - peer_addr = PeerAddr, - session_id = none, - channel = none, - connection = none, - subscriptions = #{}, - version = none, - pending_receipts = undefined, - config = Configuration, - route_state = rabbit_routing_util:init_state(), - reply_queues = #{}, - frame_transformer = undefined, - trailing_lf = application:get_env(rabbitmq_stomp, trailing_lf, true), - default_topic_exchange = application:get_env(rabbitmq_stomp, default_topic_exchange, <<"amq.topic">>), - default_nack_requeue = application:get_env(rabbitmq_stomp, default_nack_requeue, true)}. +%%---------------------------------------------------------------------------- +%% Private Parts (Including callbacks) +%%---------------------------------------------------------------------------- -command({"STOMP", Frame}, State) -> +command({'STOMP', Frame}, State) -> process_connect(no_implicit, Frame, State); -command({"CONNECT", Frame}, State) -> +command({'CONNECT', Frame}, State) -> process_connect(no_implicit, Frame, State); -command(Request, State = #proc_state{channel = none, - config = #stomp_configuration{ - implicit_connect = true}}) -> - {ok, State1 = #proc_state{channel = Ch}, _} = - process_connect(implicit, #stomp_frame{headers = []}, State), - case Ch of - none -> {stop, normal, State1}; - _ -> command(Request, State1) +command(Request, State = #state{user = undefined, + cfg = #cfg{ + implicit_connect = true}}) -> + + case process_connect(implicit, #stomp_frame{headers = []}, State) of + {ok, State1 = #state{user = undefined}} -> + {stop, normal, State1}; + {ok, State1 = #state{user = _User}} -> + command(Request, State1); + Res -> Res end; -command(_Request, State = #proc_state{channel = none, - config = #stomp_configuration{ - implicit_connect = false}}) -> +command(_Request, State = #state{user = undefined, + cfg = #cfg{ + implicit_connect = false}}) -> {ok, send_error("Illegal command", "You must log in using CONNECT first", State), none}; -command({Command, Frame}, State = #proc_state{frame_transformer = FT}) -> +command({Command, Frame}, State = #state{cfg = #cfg{frame_transformer = FT}}) -> Frame1 = FT(Frame), process_request( fun(StateN) -> - case validate_frame(Command, Frame1, StateN) of - R = {error, _, _, _} -> R; - _ -> handle_frame(Command, Frame1, StateN) - end + case validate_frame(Command, Frame1, StateN) of + R = {error, _, _, _} -> R; + _ -> handle_frame(Command, Frame1, StateN) + end end, fun(StateM) -> ensure_receipt(Frame1, StateM) end, State). -cancel_consumer(Ctag, State) -> - process_request( - fun(StateN) -> server_cancel_consumer(Ctag, StateN) end, - State). - -handle_exit(Conn, {shutdown, {server_initiated_close, Code, Explanation}}, - State = #proc_state{connection = Conn}) -> - amqp_death(Code, Explanation, State); -handle_exit(Conn, {shutdown, {connection_closing, - {server_initiated_close, Code, Explanation}}}, - State = #proc_state{connection = Conn}) -> - amqp_death(Code, Explanation, State); -handle_exit(Conn, Reason, State = #proc_state{connection = Conn}) -> - _ = send_error("AMQP connection died", "Reason: ~tp", [Reason], State), - {stop, {conn_died, Reason}, State}; - -handle_exit(Ch, {shutdown, {server_initiated_close, Code, Explanation}}, - State = #proc_state{channel = Ch}) -> - amqp_death(Code, Explanation, State); - -handle_exit(Ch, Reason, State = #proc_state{channel = Ch}) -> - _ = send_error("AMQP channel died", "Reason: ~tp", [Reason], State), - {stop, {channel_died, Reason}, State}; -handle_exit(Ch, {shutdown, {server_initiated_close, Code, Explanation}}, - State = #proc_state{channel = Ch}) -> - amqp_death(Code, Explanation, State); -handle_exit(_, _, _) -> unknown_exit. - +handle_consuming_queue_down_or_eol(QName, + State = #state{queue_consumers = QCons}) -> + %% io:format("DOE QName ~p~n", [QName]), + %% io:format("DOE QCons ~p~n", [QCons]), + ConsumerTags = case maps:find(QName, QCons) of + error -> gb_sets:new(); + {ok, CTags} -> CTags + end, + %% io:format("DOE ConsumerTags ~p~n", [ConsumerTags]), + gb_sets:fold( + fun (CTag, StateN) -> + {ok, S} = cancel_consumer(CTag, StateN), + S + end, State#state{queue_consumers = maps:remove(QName, QCons)}, ConsumerTags). + +cancel_consumer(CTag, State) -> + process_request( + fun(StateN) -> server_cancel_consumer(CTag, StateN) end, + State). process_request(ProcessFun, State) -> process_request(ProcessFun, fun (StateM) -> StateM end, State). @@ -233,23 +282,27 @@ process_request(ProcessFun, SuccessFun, State) -> {{shutdown, {server_initiated_close, ReplyCode, Explanation}}, _}} -> amqp_death(ReplyCode, Explanation, State); - {'EXIT', {amqp_error, access_refused, Msg, _}} -> - amqp_death(access_refused, Msg, State); + {'EXIT', {amqp_error, Name, Msg, _}} -> + %% io:format("amqp_error ~p, ~p~n", [Name, Msg]), + amqp_death(Name, Msg, State); {'EXIT', Reason} -> priv_error("Processing error", "Processing error", - Reason, State); + Reason, State); Result -> + %% io:format("ProcessFun: ~p~n", [Result]), Result end, case Res of - {ok, Frame, NewState = #proc_state{connection = Conn}} -> + {ok, Frame, NewState} -> _ = case Frame of none -> ok; _ -> send_frame(Frame, NewState) end, - {ok, SuccessFun(NewState), Conn}; - {error, Message, Detail, NewState = #proc_state{connection = Conn}} -> - {ok, send_error(Message, Detail, NewState), Conn}; + {ok, SuccessFun(NewState)}; + {ok, NewState} -> + {ok, SuccessFun(NewState)}; + {error, Message, Detail, NewState} -> + {ok, send_error(Message, Detail, NewState)}; {stop, normal, NewState} -> {stop, normal, SuccessFun(NewState)}; {stop, R, NewState} -> @@ -257,49 +310,93 @@ process_request(ProcessFun, SuccessFun, State) -> end. process_connect(Implicit, Frame, - State = #proc_state{channel = none, - config = Config, - ssl_login_name = SSLLoginName, - adapter_info = AdapterInfo}) -> + State = #state{user = undefined, + cfg = Config = #cfg{ + peer_ip_addr = PeerIp, + ssl_login_name = SSLLoginName, + adapter_info = AdapterInfo}}) -> process_request( fun(StateN) -> - case negotiate_version(Frame) of - {ok, Version} -> - FT = frame_transformer(Version), - Frame1 = FT(Frame), - {Auth, {Username, Passwd}} = creds(Frame1, SSLLoginName, Config), - {ok, DefaultVHost} = application:get_env( - rabbitmq_stomp, default_vhost), - {ProtoName, _} = AdapterInfo#amqp_adapter_info.protocol, - Res = do_login( - Username, Passwd, - login_header(Frame1, ?HEADER_HOST, DefaultVHost), - login_header(Frame1, ?HEADER_HEART_BEAT, "0,0"), - AdapterInfo#amqp_adapter_info{ - protocol = {ProtoName, Version}}, Version, - StateN#proc_state{frame_transformer = FT, - auth_mechanism = Auth, - auth_login = Username}), - case {Res, Implicit} of - {{ok, _, StateN1}, implicit} -> ok(StateN1); - _ -> Res - end; + Res1 = maybe + {ok, Version} = negotiate_version(Frame), + FT = frame_transformer(Version), + Frame1 = FT(Frame), + {Auth, {Username, _}} = Creds = creds(Frame1, SSLLoginName, Config), + {ok, DefaultVHost} = application:get_env(rabbitmq_stomp, default_vhost), + VHost = login_header(Frame1, ?HEADER_HOST, DefaultVHost), + Heartbeat = login_header(Frame1, ?HEADER_HEART_BEAT, "0,0"), + {ProtoName, _} = AdapterInfo#amqp_adapter_info.protocol, + StateN1 = StateN#state{cfg = Config#cfg{vhost = VHost, + adapter_info = AdapterInfo#amqp_adapter_info{ + protocol = {ProtoName, Version}}, + frame_transformer = FT, + auth_mechanism = Auth, + auth_login = Username}}, + {Username, AuthProps} = auth_props_for_creds(Creds, StateN1), + {ok, User} ?= rabbit_access_control:check_user_login(Username, AuthProps), + {ok, AuthzCtx} ?= check_vhost_access(VHost, User, PeerIp), + ok ?= check_user_loopback(Username, PeerIp), + rabbit_core_metrics:auth_attempt_succeeded(PeerIp, Username, stomp), + SessionId = rabbit_guid:string(rabbit_guid:gen_secure(), "session"), + {SendTimeout, ReceiveTimeout} = ensure_heartbeats(Heartbeat), + + Headers = [{?HEADER_SESSION, SessionId}, + {?HEADER_HEART_BEAT, + io_lib:format("~B,~B", [SendTimeout, ReceiveTimeout])}, + {?HEADER_VERSION, Version}], + + Res = ok("CONNECTED", + case application:get_env(rabbitmq_stomp, hide_server_info, false) of + true -> Headers; + false -> [{?HEADER_SERVER, server_header()} | Headers] + end, + "", + StateN1#state{cfg = StateN1#state.cfg#cfg{ + session_id = SessionId, + version = Version + }, + user = User, + authz_ctx = AuthzCtx}), + self() ! connection_created, + Res + else {error, no_common_version} -> error("Version mismatch", "Supported versions are ~ts~n", [string:join(?SUPPORTED_VERSIONS, ",")], - StateN) + StateN); + {error, not_allowed, EUsername, EVHost} -> + rabbit_log:warning("STOMP login failed for user '~ts': " + "virtual host access not allowed", [EUsername]), + error("Bad CONNECT", "Virtual host '" ++ + binary_to_list(EVHost) ++ + "' access denied", State); + {refused, Username1, _Msg, _Args} -> + rabbit_log:warning("STOMP login failed for user '~ts': authentication failed", [Username1]), + error("Bad CONNECT", "Access refused for user '" ++ + binary_to_list(Username1) ++ "'", [], State); + {error, not_loopback, EUsername} -> + rabbit_log:warning("STOMP login failed for user '~ts': " + "this user's access is restricted to localhost", [EUsername]), + error("Bad CONNECT", "non-loopback access denied", State) + end, + case {Res1, Implicit} of + {{ok, _, StateN2}, implicit} -> + self() ! connection_created, ok(StateN2); + _ -> + self() ! connection_created, Res1 + end end, State). -creds(_, _, #stomp_configuration{default_login = DefLogin, - default_passcode = DefPasscode, - force_default_creds = true}) -> +creds(_, _, #cfg{default_login = DefLogin, + default_passcode = DefPasscode, + force_default_creds = true}) -> {config, {iolist_to_binary(DefLogin), iolist_to_binary(DefPasscode)}}; creds(Frame, SSLLoginName, - #stomp_configuration{default_login = DefLogin, - default_passcode = DefPasscode}) -> + #cfg{default_login = DefLogin, + default_passcode = DefPasscode}) -> PasswordCreds = {login_header(Frame, ?HEADER_LOGIN, DefLogin), login_header(Frame, ?HEADER_PASSCODE, DefPasscode)}, case {rabbit_stomp_frame:header(Frame, ?HEADER_LOGIN), SSLLoginName} of @@ -308,6 +405,14 @@ creds(Frame, SSLLoginName, _ -> {stomp_headers, PasswordCreds} end. +auth_props_for_creds(Creds, #state{cfg = #cfg{ + vhost = VHost}}) -> + case Creds of + {ssl, {Username0, none}}-> {Username0, []}; + {_, {Username0, Password}} -> {Username0, [{password, Password}, + {vhost, VHost}]} + end. + login_header(Frame, Key, Default) when is_binary(Default) -> login_header(Frame, Key, binary_to_list(Default)); login_header(Frame, Key, Default) -> @@ -332,7 +437,7 @@ report_missing_id_header(State) -> "Header 'id' is required for durable subscriptions", State). validate_frame(Command, Frame, State) - when Command =:= "SUBSCRIBE" orelse Command =:= "UNSUBSCRIBE" -> + when Command =:= 'SUBSCRIBE' orelse Command =:= 'UNSUBSCRIBE' -> Hdr = fun(Name) -> rabbit_stomp_frame:header(Frame, Name) end, case {Hdr(?HEADER_DURABLE), Hdr(?HEADER_PERSISTENT), Hdr(?HEADER_ID)} of {{ok, "true"}, _, not_found} -> @@ -349,36 +454,50 @@ validate_frame(_Command, _Frame, State) -> %% Frame handlers %%---------------------------------------------------------------------------- -handle_frame("DISCONNECT", _Frame, State) -> +handle_frame('DISCONNECT', _Frame, State) -> {stop, normal, close_connection(State)}; -handle_frame("SUBSCRIBE", Frame, State) -> - with_destination("SUBSCRIBE", Frame, State, fun do_subscribe/4); +handle_frame('SUBSCRIBE', Frame, State) -> + with_destination('SUBSCRIBE', Frame, State, fun do_subscribe/4); -handle_frame("UNSUBSCRIBE", Frame, State) -> +handle_frame('UNSUBSCRIBE', Frame, State) -> ConsumerTag = rabbit_stomp_util:consumer_tag(Frame), cancel_subscription(ConsumerTag, Frame, State); -handle_frame("SEND", Frame, State) -> - without_headers(?HEADERS_NOT_ON_SEND, "SEND", Frame, State, - fun (_Command, Frame1, State1) -> - with_destination("SEND", Frame1, State1, fun do_send/4) - end); - -handle_frame("ACK", Frame, State) -> - ack_action("ACK", Frame, State, fun create_ack_method/3); +handle_frame('SEND', Frame, State) -> + maybe_with_transaction( + Frame, + fun(State0) -> + ensure_no_headers(?HEADERS_NOT_ON_SEND, 'SEND', Frame, State0, + fun (_Command, Frame1, State1) -> + with_destination('SEND', Frame1, State1, fun do_send/4) + end) + end, State); + +handle_frame('ACK', Frame, State) -> + maybe_with_transaction( + Frame, + fun(State0) -> + ack_action('ACK', Frame, State0, fun handle_ack/4) + end, + State); -handle_frame("NACK", Frame, State) -> - ack_action("NACK", Frame, State, fun create_nack_method/3); +handle_frame('NACK', Frame, State) -> + maybe_with_transaction( + Frame, + fun(State0) -> + ack_action('NACK', Frame, State0, fun handle_nack/4) + end, + State); -handle_frame("BEGIN", Frame, State) -> - transactional_action(Frame, "BEGIN", fun begin_transaction/2, State); +handle_frame('BEGIN', Frame, State) -> + transactional_action(Frame, 'BEGIN', fun begin_transaction/2, State); -handle_frame("COMMIT", Frame, State) -> - transactional_action(Frame, "COMMIT", fun commit_transaction/2, State); +handle_frame('COMMIT', Frame, State) -> + transactional_action(Frame, 'COMMIT', fun commit_transaction/2, State); -handle_frame("ABORT", Frame, State) -> - transactional_action(Frame, "ABORT", fun abort_transaction/2, State); +handle_frame('ABORT', Frame, State) -> + transactional_action(Frame, 'ABORT', fun abort_transaction/2, State); handle_frame(Command, _Frame, State) -> error("Bad command", @@ -391,27 +510,22 @@ handle_frame(Command, _Frame, State) -> %%---------------------------------------------------------------------------- ack_action(Command, Frame, - State = #proc_state{subscriptions = Subs, - channel = Channel, - version = Version, - default_nack_requeue = DefaultNackRequeue}, MethodFun) -> + State = #state{subscriptions = Subs, + cfg = #cfg{ + version = Version, + default_nack_requeue = DefaultNackRequeue}}, Fun) -> AckHeader = rabbit_stomp_util:ack_header_name(Version), case rabbit_stomp_frame:header(Frame, AckHeader) of {ok, AckValue} -> case rabbit_stomp_util:parse_message_id(AckValue) of {ok, {ConsumerTag, _SessionId, DeliveryTag}} -> + %% io:format("ConsumerTag ~p, DeliveryTag ~p~n", [ConsumerTag, DeliveryTag]), case maps:find(ConsumerTag, Subs) of {ok, Sub} -> + %% io:format("Sub ~p~n", [Sub]), Requeue = rabbit_stomp_frame:boolean_header(Frame, "requeue", DefaultNackRequeue), - Method = MethodFun(DeliveryTag, Sub, Requeue), - case transactional(Frame) of - {yes, Transaction} -> - extend_transaction( - Transaction, {Method}, State); - no -> - amqp_channel:call(Channel, Method), - ok(State) - end; + State1 = Fun(DeliveryTag, Sub, Requeue, State), + ok(State1); error -> error("Subscription not found", "Message with id ~tp has no subscription", @@ -419,10 +533,10 @@ ack_action(Command, Frame, State) end; _ -> - error("Invalid header", - "~tp must include a valid ~tp header~n", - [Command, AckHeader], - State) + error("Invalid header", + "~tp must include a valid ~tp header~n", + [Command, AckHeader], + State) end; not_found -> error("Missing header", @@ -435,7 +549,7 @@ ack_action(Command, Frame, %% Internal helpers for processing frames callbacks %%---------------------------------------------------------------------------- -server_cancel_consumer(ConsumerTag, State = #proc_state{subscriptions = Subs}) -> +server_cancel_consumer(ConsumerTag, State = #state{subscriptions = Subs}) -> case maps:find(ConsumerTag, Subs) of error -> error("Server cancelled unknown subscription", @@ -469,8 +583,9 @@ cancel_subscription({error, _}, _Frame, State) -> State); cancel_subscription({ok, ConsumerTag, Description}, Frame, - State = #proc_state{subscriptions = Subs, - channel = Channel}) -> + State = #state{subscriptions = Subs, + user = #user{username = Username}, + queue_states = QueueStates0}) -> case maps:find(ConsumerTag, Subs) of error -> error("No subscription found", @@ -478,56 +593,90 @@ cancel_subscription({ok, ConsumerTag, Description}, Frame, "Subscription to ~tp not found.~n", [Description], State); - {ok, Subscription = #subscription{description = Descr}} -> - case amqp_channel:call(Channel, - #'basic.cancel'{ - consumer_tag = ConsumerTag}) of - #'basic.cancel_ok'{consumer_tag = ConsumerTag} -> - tidy_canceled_subscription(ConsumerTag, Subscription, - Frame, State); - _ -> - error("Failed to cancel subscription", - "UNSUBSCRIBE to ~tp failed.~n", - [Descr], - State) + {ok, Subscription = #subscription{queue_name = Queue}} -> + + case rabbit_misc:with_exit_handler( + fun () -> {error, not_found} end, + fun () -> + %% default NoWait is false, so was the basic.cancel here + %% however there is no cancel.ok in the STOMP world + %% so OkMsg is undefined + rabbit_amqqueue:with_or_die( + Queue, + fun(Q1) -> + rabbit_queue_type:cancel( + Q1, ConsumerTag, undefined, + Username, QueueStates0) + end) + end) of + {ok, QueueStates} -> + %% rabbit_global_counters:consumer_deleted('STOMP'), + + {ok, _, NewState} = tidy_canceled_subscription(ConsumerTag, Subscription, + Frame, State#state{queue_states = QueueStates}), + {ok, NewState}; + {error, not_found} -> + %% rabbit_global_counters:consumer_deleted('STOMP'), + + {ok, _, NewState} = tidy_canceled_subscription(ConsumerTag, Subscription, + Frame, State), + {ok, NewState} end end. %% Server-initiated cancelations will pass an undefined instead of a %% STOMP frame. In this case we know that the queue was deleted and %% thus we don't have to clean it up. -tidy_canceled_subscription(ConsumerTag, _Subscription, - undefined, State = #proc_state{subscriptions = Subs}) -> - Subs1 = maps:remove(ConsumerTag, Subs), - ok(State#proc_state{subscriptions = Subs1}); +tidy_canceled_subscription(ConsumerTag, Subscription, + undefined, State) -> + tidy_canceled_subscription_state(ConsumerTag, Subscription, State); %% Client-initiated cancelations will pass an actual frame -tidy_canceled_subscription(ConsumerTag, #subscription{dest_hdr = DestHdr}, - Frame, State = #proc_state{subscriptions = Subs}) -> - Subs1 = maps:remove(ConsumerTag, Subs), - {ok, Dest} = rabbit_routing_parser:parse_endpoint(DestHdr), - maybe_delete_durable_sub(Dest, Frame, State#proc_state{subscriptions = Subs1}). -maybe_delete_durable_sub({topic, Name}, Frame, - State = #proc_state{channel = Channel}) -> +tidy_canceled_subscription(ConsumerTag, Subscription = #subscription{dest_hdr = DestHdr}, + Frame, State0) -> + {ok, State1} = tidy_canceled_subscription_state(ConsumerTag, Subscription, State0), + {ok, Dest} = parse_endpoint(DestHdr), + maybe_delete_durable_sub_queue(Dest, Frame, State1). + +tidy_canceled_subscription_state(ConsumerTag, + _Subscription = #subscription{queue_name = QName}, + State = #state{subscriptions = Subs, + queue_consumers = QCons}) -> + Subs1 = maps:remove(ConsumerTag, Subs), + QCons1 = + case maps:find(QName, QCons) of + error -> QCons; + {ok, CTags} -> CTags1 = gb_sets:delete(ConsumerTag, CTags), + case gb_sets:is_empty(CTags1) of + true -> maps:remove(QName, QCons); + false -> maps:put(QName, CTags1, QCons) + end + end, + {ok, State#state{subscriptions = Subs1, + queue_consumers = QCons1}}. + +maybe_delete_durable_sub_queue({topic, Name}, Frame, + State = #state{cfg = #cfg{auth_login = Username, + vhost = VHost}}) -> case rabbit_stomp_util:has_durable_header(Frame) of true -> {ok, Id} = rabbit_stomp_frame:header(Frame, ?HEADER_ID), QName = rabbit_stomp_util:subscription_queue_name(Name, Id, Frame), - amqp_channel:call(Channel, - #'queue.delete'{queue = list_to_binary(QName), - nowait = false}), + QRes = rabbit_misc:r(VHost, queue, list_to_binary(QName)), + ?LOG_DEBUG("Durable QRes: ~p~n", [QRes]), + delete_queue(QRes, Username), ok(State); false -> ok(State) end; -maybe_delete_durable_sub(_Destination, _Frame, State) -> +maybe_delete_durable_sub_queue(_Destination, _Frame, State) -> ok(State). with_destination(Command, Frame, State, Fun) -> case rabbit_stomp_frame:header(Frame, ?HEADER_DESTINATION) of {ok, DestHdr} -> - case rabbit_routing_parser:parse_endpoint(DestHdr) of + case parse_endpoint(DestHdr) of {ok, Destination} -> case Fun(Destination, DestHdr, Frame, State) of {error, invalid_endpoint} -> @@ -555,8 +704,7 @@ with_destination(Command, Frame, State, Fun) -> "'~ts' is not a valid destination.~n" "Valid destination types are: ~ts.~n", [Content, - string:join(rabbit_routing_util:all_dest_prefixes(), - ", ")], State) + string:join(?ALL_DEST_PREFIXES, ", ")], State) end; not_found -> error("Missing destination", @@ -565,7 +713,7 @@ with_destination(Command, Frame, State, Fun) -> State) end. -without_headers([Hdr | Hdrs], Command, Frame, State, Fun) -> +ensure_no_headers([Hdr | Hdrs], Command, Frame, State, Fun) -> % case rabbit_stomp_frame:header(Frame, Hdr) of {ok, _} -> error("Invalid header", @@ -573,99 +721,30 @@ without_headers([Hdr | Hdrs], Command, Frame, State, Fun) -> [Hdr, Command], State); not_found -> - without_headers(Hdrs, Command, Frame, State, Fun) + ensure_no_headers(Hdrs, Command, Frame, State, Fun) end; -without_headers([], Command, Frame, State, Fun) -> +ensure_no_headers([], Command, Frame, State, Fun) -> Fun(Command, Frame, State). -do_login(undefined, _, _, _, _, _, State) -> - error("Bad CONNECT", "Missing login or passcode header(s)", State); -do_login(Username, Passwd, VirtualHost, Heartbeat, AdapterInfo, Version, - State = #proc_state{peer_addr = Addr}) -> - case start_connection( - #amqp_params_direct{username = Username, - password = Passwd, - virtual_host = VirtualHost, - adapter_info = AdapterInfo}, Username, Addr) of - {ok, Connection} -> - link(Connection), - {ok, Channel} = amqp_connection:open_channel(Connection), - link(Channel), - amqp_channel:enable_delivery_flow_control(Channel), - SessionId = rabbit_guid:string(rabbit_guid:gen_secure(), "session"), - {SendTimeout, ReceiveTimeout} = ensure_heartbeats(Heartbeat), - - Headers = [{?HEADER_SESSION, SessionId}, - {?HEADER_HEART_BEAT, - io_lib:format("~B,~B", [SendTimeout, ReceiveTimeout])}, - {?HEADER_VERSION, Version}], - ok("CONNECTED", - case application:get_env(rabbitmq_stomp, hide_server_info, false) of - true -> Headers; - false -> [{?HEADER_SERVER, server_header()} | Headers] - end, - "", - State#proc_state{session_id = SessionId, - channel = Channel, - connection = Connection, - version = Version}); - {error, {auth_failure, _}} -> - rabbit_log:warning("STOMP login failed for user '~ts': authentication failed", [Username]), - error("Bad CONNECT", "Access refused for user '" ++ - binary_to_list(Username) ++ "'", [], State); - {error, not_allowed} -> - rabbit_log:warning("STOMP login failed for user '~ts': " - "virtual host access not allowed", [Username]), - error("Bad CONNECT", "Virtual host '" ++ - binary_to_list(VirtualHost) ++ - "' access denied", State); - {error, access_refused} -> - rabbit_log:warning("STOMP login failed for user '~ts': " - "virtual host access not allowed", [Username]), - error("Bad CONNECT", "Virtual host '" ++ - binary_to_list(VirtualHost) ++ - "' access denied", State); - {error, not_loopback} -> - rabbit_log:warning("STOMP login failed for user '~ts': " - "this user's access is restricted to localhost", [Username]), - error("Bad CONNECT", "non-loopback access denied", State) - end. - -start_connection(Params, Username, Addr) -> - case amqp_connection:start(Params) of - {ok, Conn} -> case rabbit_access_control:check_user_loopback( - Username, Addr) of - ok -> {ok, Conn}; - not_allowed -> amqp_connection:close(Conn), - {error, not_loopback} - end; - {error, E} -> {error, E} - end. - server_header() -> {ok, Product} = application:get_key(rabbit, description), {ok, Version} = application:get_key(rabbit, vsn), rabbit_misc:format("~ts/~ts", [Product, Version]). do_subscribe(Destination, DestHdr, Frame, - State = #proc_state{subscriptions = Subs, - route_state = RouteState, - channel = Channel, - default_topic_exchange = DfltTopicEx}) -> - check_subscription_access(Destination, State), + State0 = #state{subscriptions = Subs, + cfg = #cfg{default_topic_exchange = DfltTopicEx}, + queue_consumers = QCons}) -> + check_subscription_access(Destination, State0), + + {ok, {_Global, DefaultPrefetch}} = application:get_env(rabbit, default_consumer_prefetch), Prefetch = - rabbit_stomp_frame:integer_header(Frame, ?HEADER_PREFETCH_COUNT, - undefined), + rabbit_stomp_frame:integer_header(Frame, ?HEADER_PREFETCH_COUNT, DefaultPrefetch), + %% io:format("Prefetch: ~p~n", [Prefetch]), {AckMode, IsMulti} = rabbit_stomp_util:ack_mode(Frame), - case ensure_endpoint(source, Destination, Frame, Channel, RouteState) of - {ok, Queue, RouteState1} -> - {ok, ConsumerTag, Description} = - rabbit_stomp_util:consumer_tag(Frame), - case Prefetch of - undefined -> ok; - _ -> amqp_channel:call( - Channel, #'basic.qos'{prefetch_count = Prefetch}) - end, + case ensure_endpoint(source, Destination, Frame, State0) of + {ok, QueueName, State} -> + {ok, ConsumerTag, Description} = rabbit_stomp_util:consumer_tag(Frame), case maps:find(ConsumerTag, Subs) of {ok, _} -> Message = "Duplicated subscription identifier", @@ -677,39 +756,41 @@ do_subscribe(Destination, DestHdr, Frame, ExchangeAndKey = parse_routing(Destination, DfltTopicEx), Arguments = subscribe_arguments(Frame), try - amqp_channel:subscribe(Channel, - #'basic.consume'{ - queue = Queue, - consumer_tag = ConsumerTag, - no_local = false, - no_ack = (AckMode == auto), - exclusive = false, - arguments = Arguments}, - self()), - ok = rabbit_routing_util:ensure_binding( - Queue, ExchangeAndKey, Channel) + {ok, State1} = consume_queue(QueueName, #{no_ack => (AckMode == auto), + prefetch_count => Prefetch, + mode => {simple_prefetch, Prefetch}, + consumer_tag => ConsumerTag, + exclusive_consume => false, + args => Arguments}, + State), + ok = ensure_binding(QueueName, ExchangeAndKey, State1), + CTags1 = case maps:find(QueueName, QCons) of + {ok, CTags} -> gb_sets:insert(ConsumerTag, CTags); + error -> gb_sets:singleton(ConsumerTag) + end, + QCons1 = maps:put(QueueName, CTags1, QCons), + ok(State1#state{subscriptions = maps:put( + ConsumerTag, + #subscription{dest_hdr = DestHdr, + ack_mode = AckMode, + multi_ack = IsMulti, + description = Description, + queue_name = QueueName}, + Subs), + queue_consumers = QCons1}) catch exit:Err -> %% it's safe to delete this queue, it %% was server-named and declared by us case Destination of {exchange, _} -> - ok = maybe_clean_up_queue(Queue, State); + ok = maybe_clean_up_queue(QueueName, State); {topic, _} -> - ok = maybe_clean_up_queue(Queue, State); + ok = maybe_clean_up_queue(QueueName, State); _ -> ok end, exit(Err) - end, - ok(State#proc_state{subscriptions = - maps:put( - ConsumerTag, - #subscription{dest_hdr = DestHdr, - ack_mode = AckMode, - multi_ack = IsMulti, - description = Description}, - Subs), - route_state = RouteState1}) + end end; {error, _} = Err -> Err @@ -761,78 +842,338 @@ subscribe_argument(?HEADER_X_PRIORITY, Frame, Acc) -> end. check_subscription_access(Destination = {topic, _Topic}, - #proc_state{auth_login = _User, - connection = Connection, - default_topic_exchange = DfltTopicEx}) -> - [{amqp_params, AmqpParams}, {internal_user, InternalUser = #user{username = Username}}] = - amqp_connection:info(Connection, [amqp_params, internal_user]), - #amqp_params_direct{virtual_host = VHost} = AmqpParams, + #state{user = #user{username = Username} = User, + cfg = #cfg{ + default_topic_exchange = DfltTopicEx, + vhost = VHost}}) -> {Exchange, RoutingKey} = parse_routing(Destination, DfltTopicEx), Resource = #resource{virtual_host = VHost, - kind = topic, - name = rabbit_data_coercion:to_binary(Exchange)}, + kind = topic, + name = rabbit_data_coercion:to_binary(Exchange)}, Context = #{routing_key => rabbit_data_coercion:to_binary(RoutingKey), variable_map => #{<<"vhost">> => VHost, <<"username">> => Username} - }, - rabbit_access_control:check_topic_access(InternalUser, Resource, read, Context); + }, + rabbit_access_control:check_topic_access(User, Resource, read, Context); check_subscription_access(_, _) -> authorized. -maybe_clean_up_queue(Queue, #proc_state{connection = Connection}) -> - {ok, Channel} = amqp_connection:open_channel(Connection), - catch amqp_channel:call(Channel, #'queue.delete'{queue = Queue}), - catch amqp_channel:close(Channel), +maybe_clean_up_queue(Queue, #state{cfg = #cfg{auth_login = Username}}) -> + catch delete_queue(Queue, Username), ok. do_send(Destination, _DestHdr, - Frame = #stomp_frame{body_iolist = BodyFragments}, - State = #proc_state{channel = Channel, - route_state = RouteState, - default_topic_exchange = DfltTopicEx}) -> - case ensure_endpoint(dest, Destination, Frame, Channel, RouteState) of - - {ok, _Q, RouteState1} -> - + Frame = #stomp_frame{body_iolist_rev = BodyFragments}, + State0 = #state{ + user = User, + authz_ctx = AuthzCtx, + cfg = #cfg{ + delivery_flow = Flow, + default_topic_exchange = DfltTopicEx, + vhost = VHost}}) -> + case ensure_endpoint(dest, Destination, Frame, State0) of + + {ok, _Q, State} -> {Frame1, State1} = - ensure_reply_to(Frame, State#proc_state{route_state = RouteState1}), + ensure_reply_to(Frame, State), Props = rabbit_stomp_util:message_properties(Frame1), - {Exchange, RoutingKey} = parse_routing(Destination, DfltTopicEx), - - Method = #'basic.publish'{ - exchange = list_to_binary(Exchange), - routing_key = list_to_binary(RoutingKey), - mandatory = false, - immediate = false}, - - case transactional(Frame1) of - {yes, Transaction} -> - extend_transaction( - Transaction, - fun(StateN) -> - maybe_record_receipt(Frame1, StateN) - end, - {Method, Props, BodyFragments}, - State1); - no -> - ok(send_method(Method, Props, BodyFragments, - maybe_record_receipt(Frame1, State1))) - end; + {ExchangeNameList, RoutingKeyList} = parse_routing(Destination, DfltTopicEx), + %% io:format("Parse_routing: ~p~n", [{ExchangeNameList, RoutingKeyList}]), + RoutingKey = list_to_binary(RoutingKeyList), + + + rabbit_global_counters:messages_received(stomp, 1), + + ExchangeName = rabbit_misc:r(VHost, exchange, list_to_binary(ExchangeNameList)), + check_resource_access(User, ExchangeName, write, AuthzCtx), + Exchange = rabbit_exchange:lookup_or_die(ExchangeName), + check_internal_exchange(Exchange), + check_topic_authorisation(Exchange, User, RoutingKey, AuthzCtx, write), + + {DeliveryOptions, _MsgSeqNo, State2} = + case rabbit_stomp_frame:header(Frame, ?HEADER_RECEIPT) of + not_found -> + {maps_put_truthy(flow, Flow, #{}), undefined, State1}; + {ok, Id} -> + rabbit_global_counters:messages_received_confirm(stomp, 1), + SeqNo = State1#state.msg_seq_no, + %% I think it's safe to just add it here because + %% if there is an error down the road process dies + StateRR = record_receipt(true, SeqNo, Id, State1), + Opts = maps_put_truthy(flow, Flow, #{correlation => SeqNo}), + {Opts, SeqNo, StateRR#state{msg_seq_no = SeqNo + 1}} + end, - {error, _} = Err -> + {ClassId, _MethodId} = rabbit_framing_amqp_0_9_1:method_id('basic.publish'), + Content0 = #content{ + class_id = ClassId, + properties = Props, + properties_bin = none, + protocol = none, + payload_fragments_rev = BodyFragments + }, + + {ok, Message0} = mc_amqpl:message(ExchangeName, RoutingKey, Content0), + + Message = rabbit_message_interceptor:intercept(Message0), + + %% io:format("Message: ~p~n", [Message]), + + QNames = rabbit_exchange:route(Exchange, Message, #{return_binding_keys => true}), + %% io:format("QNames ~p~n", [QNames]), + + Delivery = {Message, DeliveryOptions, QNames}, + %% io:format("Delivery: ~p~n", [Delivery]), + deliver_to_queues(ExchangeName, Delivery, State2); + {error, _} = Err -> + %% io:format("Err ~p~n", [Err]), Err end. -create_ack_method(DeliveryTag, #subscription{multi_ack = IsMulti}, _) -> - #'basic.ack'{delivery_tag = DeliveryTag, - multiple = IsMulti}. +deliver_to_queues(_XName, + {_Message, Options, _RoutedToQueues = []}, + State) + when not is_map_key(correlation, Options) -> %% optimisation when there are no queues + %%?INCR_STATS(exchange_stats, XName, 1, publish, State), + rabbit_global_counters:messages_unroutable_dropped(stomp, 1), + %%?INCR_STATS(exchange_stats, XName, 1, drop_unroutable, State), + {ok, State}; + +deliver_to_queues(XName, + {Message, Options, RoutedToQNames}, + State0 = #state{queue_states = QStates0}) -> + Qs0 = rabbit_amqqueue:lookup_many(RoutedToQNames), + Qs = rabbit_amqqueue:prepend_extra_bcc(Qs0), + MsgSeqNo = maps:get(correlation, Options, undefined), + %% io:format("Qs: ~p~n", [Qs]), + case rabbit_queue_type:deliver(Qs, Message, Options, QStates0) of + {ok, QStates, Actions} -> + rabbit_global_counters:messages_routed(stomp, length(Qs)), + QueueNames = rabbit_amqqueue:queue_names(Qs), + State1 = process_routing_confirm(MsgSeqNo, QueueNames, XName, State0), + %% Actions must be processed after registering confirms as actions may + %% contain rejections of publishes. + {ok, handle_queue_actions(Actions, State1#state{queue_states = QStates})}; + {error, Reason} -> + log_error("Failed to deliver message with packet_id=~p to queues: ~p", + [MsgSeqNo, Reason], none), + {error, Reason, State0} + end. + -create_nack_method(DeliveryTag, #subscription{multi_ack = IsMulti}, Requeue) -> - #'basic.nack'{delivery_tag = DeliveryTag, - multiple = IsMulti, - requeue = Requeue}. +record_rejects([], State) -> + State; +record_rejects(MXs, State = #state{rejected = R%% , tx = Tx + }) -> + %% Tx1 = case Tx of + %% none -> none; + %% _ -> failed + %% end, + State#state{rejected = [MXs | R]%% , tx = Tx1 + }. + +record_confirms([], State) -> + State; +record_confirms(MXs, State = #state{confirmed = C}) -> + State#state{confirmed = [MXs | C]}. + +process_routing_confirm(undefined, _, _, State) -> + State; +process_routing_confirm(MsgSeqNo, [], XName, State) -> + record_confirms([{MsgSeqNo, XName}], State); +process_routing_confirm(MsgSeqNo, QRefs, XName, State) -> + State#state{unconfirmed = + rabbit_confirms:insert(MsgSeqNo, QRefs, XName, State#state.unconfirmed)}. + +confirm(MsgSeqNos, QRef, State = #state{unconfirmed = UC}) -> + %% NOTE: if queue name does not exist here it's likely that the ref also + %% does not exist in unconfirmed messages. + %% Neither does the 'ignore' atom, so it's a reasonable fallback. + {ConfirmMXs, UC1} = rabbit_confirms:confirm(MsgSeqNos, QRef, UC), + %% NB: don't call noreply/1 since we don't want to send confirms. + record_confirms(ConfirmMXs, State#state{unconfirmed = UC1}). + +send_confirms_and_nacks(State = #state{%% tx = none, + confirmed = [], rejected = []}) -> + State; +send_confirms_and_nacks(State = #state{%% tx = none, + confirmed = C, rejected = R}) -> + case rabbit_node_monitor:pause_partition_guard() of + ok -> + Confirms = lists:append(C), + %% rabbit_global_counters:messages_confirmed('STOMP', length(Confirms)), + Rejects = lists:append(R), + ConfirmMsgSeqNos = + lists:foldl( + fun ({MsgSeqNo, _XName}, MSNs) -> + %% ?INCR_STATS(exchange_stats, XName, 1, confirm, State), %% TODO: what to do with stats + [MsgSeqNo | MSNs] + end, [], Confirms), + RejectMsgSeqNos = [MsgSeqNo || {MsgSeqNo, _} <- Rejects], + + State1 = send_confirms(ConfirmMsgSeqNos, + RejectMsgSeqNos, + State#state{confirmed = []}), + %% TODO: we don't have server-originated nacks in STOMP unfortunately + %% TODO: msg seq nos, same as for confirms. Need to implement + %% nack rates first. + %% send_nacks(RejectMsgSeqNos, + %% ConfirmMsgSeqNos, + %% State1#state{rejected = []}); + State1#state{rejected = []}; + pausing -> State + end. + +%% TODO: in stomp we can only ERROR, there is no commit_ok :-( +%% send_confirms_and_nacks(State) -> +%% case rabbit_node_monitor:pause_partition_guard() of +%% ok -> maybe_complete_tx(State); +%% pausing -> State +%% end +%% . + +%% TODO: in stomp there is no nacks, only ERROR, shall I send error here?? +%% send_nacks([], _, State) -> +%% State; +%% send_nacks(_Rs, _, State = #ch{cfg = #conf{state = closing}}) -> %% optimisation +%% State; +%% send_nacks(Rs, Cs, State) -> +%% coalesce_and_send(Rs, Cs, +%% fun(MsgSeqNo, Multiple) -> +%% #'basic.nack'{delivery_tag = MsgSeqNo, +%% multiple = Multiple} +%% end, State). + +send_confirms([], _, State) -> + State; +%% TODO: implement connection states +%% send_confirms(_Cs, _, State = #ch{cfg = #conf{state = closing}}) -> %% optimisation +%% State; +send_confirms([MsgSeqNo], _, State) -> + State1 = flush_pending_receipts(MsgSeqNo, false, State), + State1; +send_confirms(Cs, Rs, State) -> + coalesce_and_send(Cs, Rs, + fun(MsgSeqNo, Multiple, StateN) -> + flush_pending_receipts(MsgSeqNo, Multiple, StateN) + end, State). + +coalesce_and_send(MsgSeqNos, NegativeMsgSeqNos, MkMsgFun, State = #state{unconfirmed = UC}) -> + SMsgSeqNos = lists:usort(MsgSeqNos), + UnconfirmedCutoff = case rabbit_confirms:is_empty(UC) of + true -> lists:last(SMsgSeqNos) + 1; + false -> rabbit_confirms:smallest(UC) + end, + Cutoff = lists:min([UnconfirmedCutoff | NegativeMsgSeqNos]), + {Ms, Ss} = lists:splitwith(fun(X) -> X < Cutoff end, SMsgSeqNos), + State1 = case Ms of + [] -> State; + _ -> MkMsgFun(lists:last(Ms), true, State) + end, + lists:foldl(fun(SeqNo, StateN) -> + MkMsgFun(SeqNo, false, StateN) + end, State1, Ss). + +%% ack_len(Acks) -> lists:sum([length(L) || {ack, L} <- Acks]). + +handle_ack(DeliveryTag, #subscription{multi_ack = IsMulti}, _, State = #state{unacked_message_q = UAMQ}) -> + %% io:format("UAMQ ~p~n", [UAMQ]), + {Acked, Remaining} = collect_acks(UAMQ, DeliveryTag, IsMulti), + %% io:format("Acked ~p~n, Remaining ~p~n", [Acked, Remaining]), + State1 = State#state{unacked_message_q = Remaining}, + {State2, Actions} = settle_acks(Acked, State1), + handle_queue_actions(Actions, State2). + +handle_nack(DeliveryTag, #subscription{multi_ack = IsMulti}, Requeue, State = #state{unacked_message_q = UAMQ}) -> + %% io:format("UAMQ ~p~n", [UAMQ]), + {Acked, Remaining} = collect_acks(UAMQ, DeliveryTag, IsMulti), + %% io:format("Acked ~p~n, Remaining ~p~n", [Acked, Remaining]), + State1 = State#state{unacked_message_q = Remaining}, + {State2, Actions} = internal_reject(Requeue, Acked, State1), + handle_queue_actions(Actions, State2). + +%% Records a client-sent acknowledgement. Handles both single delivery acks +%% and multi-acks. +%% +%% Returns a tuple of acknowledged pending acks and remaining pending acks. +%% Sorts each group in the youngest-first order (descending by delivery tag). +collect_acks(UAMQ, DeliveryTag, Multiple) -> + collect_acks([], [], UAMQ, DeliveryTag, Multiple). + +collect_acks(AcknowledgedAcc, RemainingAcc, UAMQ, DeliveryTag, Multiple) -> + case ?QUEUE:out(UAMQ) of + {{value, UnackedMsg = #pending_ack{delivery_tag = CurrentDT}}, + UAMQTail} -> + if CurrentDT == DeliveryTag -> + {[UnackedMsg | AcknowledgedAcc], + case RemainingAcc of + [] -> UAMQTail; + _ -> ?QUEUE:join( + ?QUEUE:from_list(lists:reverse(RemainingAcc)), + UAMQTail) + end}; + Multiple -> + collect_acks([UnackedMsg | AcknowledgedAcc], RemainingAcc, + UAMQTail, DeliveryTag, Multiple); + true -> + collect_acks(AcknowledgedAcc, [UnackedMsg | RemainingAcc], + UAMQTail, DeliveryTag, Multiple) + end; + {empty, _} -> + error("Unknown delivery tag", + "unknown delivery tag ~w", [DeliveryTag]) + end. + +%% foreach_per_queue(_F, [], Acc) -> +%% Acc; +foreach_per_queue(F, [#pending_ack{tag = CTag, + queue = QName, + msg_id = MsgId}], Acc) -> + %% quorum queue, needs the consumer tag + F({QName, CTag}, [MsgId], Acc); +foreach_per_queue(F, UAL, Acc) -> + T = lists:foldl(fun (#pending_ack{tag = CTag, + queue = QName, + msg_id = MsgId}, T) -> + rabbit_misc:gb_trees_cons({QName, CTag}, MsgId, T) + end, gb_trees:empty(), UAL), + rabbit_misc:gb_trees_fold(fun (Key, Val, Acc0) -> F(Key, Val, Acc0) end, Acc, T). + +settle_acks(Acks, State = #state{queue_states = QueueStates0}) -> + {QueueStates, Actions} = + foreach_per_queue( + fun ({QRef, CTag}, MsgIds, {Acc0, ActionsAcc0}) -> + case rabbit_queue_type:settle(QRef, complete, CTag, + MsgIds, Acc0) of + {ok, Acc, ActionsAcc} -> + %% incr_queue_stats(QRef, MsgIds, State), + {Acc, ActionsAcc0 ++ ActionsAcc}; + {protocol_error, ErrorType, Reason, ReasonArgs} -> + rabbit_misc:protocol_error(ErrorType, Reason, ReasonArgs) + end + end, Acks, {QueueStates0, []}), + {State#state{queue_states = QueueStates}, Actions}. + +%% NB: Acked is in youngest-first order +internal_reject(Requeue, Acked, + State = #state{queue_states = QueueStates0}) -> + {QueueStates, Actions} = + foreach_per_queue( + fun({QRef, CTag}, MsgIds, {Acc0, Actions0}) -> + Op = case Requeue of + false -> discard; + true -> requeue + end, + case rabbit_queue_type:settle(QRef, Op, CTag, MsgIds, Acc0) of + {ok, Acc, Actions} -> + {Acc, Actions0 ++ Actions}; + {protocol_error, ErrorType, Reason, ReasonArgs} -> + rabbit_misc:protocol_error(ErrorType, Reason, ReasonArgs) + end + end, Acked, {QueueStates0, []}), + {State#state{queue_states = QueueStates}, Actions}. negotiate_version(Frame) -> ClientVers = re:split(rabbit_stomp_frame:header( @@ -841,65 +1182,106 @@ negotiate_version(Frame) -> rabbit_stomp_util:negotiate_version(ClientVers, ?SUPPORTED_VERSIONS). -send_delivery(Delivery = #'basic.deliver'{consumer_tag = ConsumerTag}, +deliver_to_client(ConsumerTag, Ack, Msgs, State) -> + lists:foldl(fun(Msg, S) -> + deliver_one_to_client(ConsumerTag, Ack, Msg, S) + end, State, Msgs). + +deliver_one_to_client(ConsumerTag, _Ack, {QName, QPid, MsgId, Redelivered, MsgCont0} = _Msg, + State = #state{queue_states = QStates, + delivery_tag = DeliveryTag}) -> + + [RoutingKey | _] = mc:routing_keys(MsgCont0), + ExchangeNameBin = mc:exchange(MsgCont0), + MsgCont = mc:convert(mc_amqpl, MsgCont0), + Content = mc:protocol_state(MsgCont), + Delivery = #'basic.deliver'{consumer_tag = ConsumerTag, + delivery_tag = DeliveryTag, + redelivered = Redelivered, + exchange = ExchangeNameBin, + routing_key = RoutingKey}, + + + {Props, Payload} = rabbit_basic_common:from_content(Content), + + + DeliveryCtx = case rabbit_queue_type:module(QName, QStates) of + {ok, rabbit_classic_queue} -> + {ok, QPid, ok}; + _ -> undefined + end, + + State1 = send_delivery(QName, MsgId, Delivery, Props, Payload, DeliveryCtx, State), + + State1#state{delivery_tag = DeliveryTag + 1}. + + +send_delivery(QName, MsgId, Delivery = #'basic.deliver'{consumer_tag = ConsumerTag, + delivery_tag = DeliveryTag}, Properties, Body, DeliveryCtx, - State = #proc_state{ - session_id = SessionId, - subscriptions = Subs, - version = Version}) -> - NewState = case maps:find(ConsumerTag, Subs) of + State = #state{ + cfg = #cfg{ + session_id = SessionId, + version = Version + }, + subscriptions = Subs, + unacked_message_q = UAMQ}) -> + %% io:format("SD Subs ~p~n", [Subs]), + case maps:find(ConsumerTag, Subs) of {ok, #subscription{ack_mode = AckMode}} -> - send_frame( - "MESSAGE", - rabbit_stomp_util:headers(SessionId, Delivery, Properties, - AckMode, Version), - Body, - State); + NewState = send_frame( + 'MESSAGE', + rabbit_stomp_util:headers(SessionId, Delivery, Properties, + AckMode, Version), + Body, + State), + maybe_notify_sent(DeliveryCtx), + case AckMode of + client -> + DeliveredAt = os:system_time(millisecond), + %% io:format("Send delivery state: ~p~n", [NewState#state{unacked_message_q = + %% ?QUEUE:in(#pending_ack{delivery_tag = DeliveryTag, + %% tag = ConsumerTag, + %% delivered_at = DeliveredAt, + %% queue = QName, + %% msg_id = MsgId}, UAMQ)}]), + NewState#state{unacked_message_q = + ?QUEUE:in(#pending_ack{delivery_tag = DeliveryTag, + tag = ConsumerTag, + delivered_at = DeliveredAt, + queue = QName, + msg_id = MsgId}, UAMQ)}; + _ -> NewState + end; error -> send_error("Subscription not found", "There is no current subscription with tag '~ts'.", [ConsumerTag], State) - end, - notify_received(DeliveryCtx), - NewState. - -notify_received(undefined) -> - %% no notification for quorum queues and streams - ok; -notify_received(DeliveryCtx) -> - %% notification for flow control - amqp_channel:notify_received(DeliveryCtx). - -send_method(Method, Channel, State) -> - amqp_channel:call(Channel, Method), - State. - -send_method(Method, State = #proc_state{channel = Channel}) -> - send_method(Method, Channel, State). + end. -send_method(Method, Properties, BodyFragments, - State = #proc_state{channel = Channel}) -> - send_method(Method, Channel, Properties, BodyFragments, State). +maybe_notify_sent(undefined) -> + ok; +maybe_notify_sent({_, QPid, _}) -> + ok = rabbit_amqqueue:notify_sent(QPid, self()). -send_method(Method = #'basic.publish'{}, Channel, Properties, BodyFragments, - State) -> - amqp_channel:cast_flow( - Channel, Method, - #amqp_msg{props = Properties, - payload = list_to_binary(BodyFragments)}), +close_connection(State) -> + %% TODO: I feel like there has to be a cleanup, + %% maybe delete queues we created? + %% notify queues that subsriptions are shutdown? + %% (they probably monitor self() after consume anyway) State. -close_connection(State = #proc_state{connection = none}) -> - State; -%% Closing the connection will close the channel and subchannels -close_connection(State = #proc_state{connection = Connection}) -> - %% ignore noproc or other exceptions to avoid debris - catch amqp_connection:close(Connection), - State#proc_state{channel = none, connection = none, subscriptions = none}; -close_connection(undefined) -> - rabbit_log:debug("~ts:close_connection: undefined state", [?MODULE]), - #proc_state{channel = none, connection = none, subscriptions = none}. +%% close_connection(State = #state{connection = none}) -> +%% State; +%% %% Closing the connection will close the channel and subchannels +%% close_connection(State = #state{connection = Connection}) -> +%% %% ignore noproc or other exceptions to avoid debris +%% catch amqp_connection:close(Connection), +%% State#state{channel = none, connection = none, subscriptions = none}; +%% close_connection(undefined) -> +%% rabbit_log:debug("~ts:close_connection: undefined state", [?MODULE]), +%% #state{channel = none, connection = none, subscriptions = none}. %%---------------------------------------------------------------------------- %% Reply-To @@ -910,8 +1292,8 @@ ensure_reply_to(Frame = #stomp_frame{headers = Headers}, State) -> not_found -> {Frame, State}; {ok, ReplyTo} -> - {ok, Destination} = rabbit_routing_parser:parse_endpoint(ReplyTo), - case rabbit_routing_util:dest_temp_queue(Destination) of + {ok, Destination} = parse_endpoint(ReplyTo), + case dest_temp_queue(Destination) of none -> {Frame, State}; TempQueueId -> @@ -925,29 +1307,26 @@ ensure_reply_to(Frame = #stomp_frame{headers = Headers}, State) -> end end. -ensure_reply_queue(TempQueueId, State = #proc_state{channel = Channel, - reply_queues = RQS, - subscriptions = Subs}) -> +ensure_reply_queue(TempQueueId, State = #state{reply_queues = RQS, + subscriptions = Subs}) -> case maps:find(TempQueueId, RQS) of {ok, RQ} -> {binary_to_list(RQ), State}; error -> - #'queue.declare_ok'{queue = Queue} = - amqp_channel:call(Channel, - #'queue.declare'{auto_delete = true, - exclusive = true}), + {ok, Queue} = create_queue(State), + #resource{name = QNameBin} = QName = amqqueue:get_name(Queue), ConsumerTag = rabbit_stomp_util:consumer_tag_reply_to(TempQueueId), - #'basic.consume_ok'{} = - amqp_channel:subscribe(Channel, - #'basic.consume'{ - queue = Queue, - consumer_tag = ConsumerTag, - no_ack = true, - nowait = false}, - self()), - Destination = binary_to_list(Queue), + + {ok, {_Global, DefaultPrefetch}} = application:get_env(rabbit, default_consumer_prefetch), + Spec = #{no_ack => true, + prefetch_count => DefaultPrefetch, + consumer_tag => ConsumerTag, + exclusive_consume => false, + args => []}, + {ok, State1} = consume_queue(QName, Spec, State), + Destination = binary_to_list(QNameBin), %% synthesise a subscription to the reply queue destination Subs1 = maps:put(ConsumerTag, @@ -955,8 +1334,8 @@ ensure_reply_queue(TempQueueId, State = #proc_state{channel = Channel, multi_ack = false}, Subs), - {Destination, State#proc_state{ - reply_queues = maps:put(TempQueueId, Queue, RQS), + {Destination, State1#state{ + reply_queues = maps:put(TempQueueId, QNameBin, RQS), subscriptions = Subs1}} end. @@ -965,44 +1344,28 @@ ensure_reply_queue(TempQueueId, State = #proc_state{channel = Channel, %%---------------------------------------------------------------------------- ensure_receipt(Frame = #stomp_frame{command = Command}, State) -> + %% io:format("ER Frame: ~p~n", [Frame]), case rabbit_stomp_frame:header(Frame, ?HEADER_RECEIPT) of {ok, Id} -> do_receipt(Command, Id, State); not_found -> State end. -do_receipt("SEND", _, State) -> +do_receipt('SEND', _, State) -> %% SEND frame receipts are handled when messages are confirmed State; do_receipt(_Frame, ReceiptId, State) -> - send_frame("RECEIPT", [{"receipt-id", ReceiptId}], "", State). + send_frame('RECEIPT', [{"receipt-id", ReceiptId}], "", State). -maybe_record_receipt(Frame, State = #proc_state{channel = Channel, - pending_receipts = PR}) -> - case rabbit_stomp_frame:header(Frame, ?HEADER_RECEIPT) of - {ok, Id} -> - PR1 = case PR of - undefined -> - amqp_channel:register_confirm_handler( - Channel, self()), - #'confirm.select_ok'{} = - amqp_channel:call(Channel, #'confirm.select'{}), - gb_trees:empty(); - _ -> - PR - end, - SeqNo = amqp_channel:next_publish_seqno(Channel), - State#proc_state{pending_receipts = gb_trees:insert(SeqNo, Id, PR1)}; - not_found -> - State - end. +record_receipt(_DoConfirm = true, MsgSeqNo, ReceiptId, State = #state{pending_receipts = PR}) -> + State#state{pending_receipts = gb_trees:insert(MsgSeqNo, ReceiptId, PR)}. flush_pending_receipts(DeliveryTag, IsMulti, - State = #proc_state{pending_receipts = PR}) -> + State = #state{pending_receipts = PR}) -> {Receipts, PR1} = accumulate_receipts(DeliveryTag, IsMulti, PR), State1 = lists:foldl(fun(ReceiptId, StateN) -> do_receipt(none, ReceiptId, StateN) end, State, Receipts), - State1#proc_state{pending_receipts = PR1}. + State1#state{pending_receipts = PR1}. accumulate_receipts(DeliveryTag, false, PR) -> case gb_trees:lookup(DeliveryTag, PR) of @@ -1049,6 +1412,18 @@ transactional_action(Frame, Name, Fun, State) -> State) end. +maybe_with_transaction(Frame, Fun, State) -> + case transactional(Frame) of + {yes, Transaction} -> + extend_transaction( + Transaction, + Fun, + Frame, + State); + no -> + Fun(State) + end. + with_transaction(Transaction, State, Fun) -> case get({transaction, Transaction}) of undefined -> @@ -1064,42 +1439,40 @@ begin_transaction(Transaction, State) -> put({transaction, Transaction}, []), ok(State). -extend_transaction(Transaction, Callback, Action, State) -> - extend_transaction(Transaction, {callback, Callback, Action}, State). - -extend_transaction(Transaction, Action, State0) -> +extend_transaction(Transaction, Fun, Frame, State0) -> with_transaction( Transaction, State0, - fun (Actions, State) -> - put({transaction, Transaction}, [Action | Actions]), + fun (Funs, State) -> + put({transaction, Transaction}, [{Frame, Fun} | Funs]), ok(State) end). commit_transaction(Transaction, State0) -> with_transaction( Transaction, State0, - fun (Actions, State) -> + fun (Funs, State) -> FinalState = lists:foldr(fun perform_transaction_action/2, - State, - Actions), + {ok, State}, + Funs), erase({transaction, Transaction}), - ok(FinalState) + FinalState end). abort_transaction(Transaction, State0) -> with_transaction( Transaction, State0, - fun (_Actions, State) -> + fun (_Frames, State) -> erase({transaction, Transaction}), ok(State) end). -perform_transaction_action({callback, Callback, Action}, State) -> - perform_transaction_action(Action, Callback(State)); -perform_transaction_action({Method}, State) -> - send_method(Method, State); -perform_transaction_action({Method, Props, BodyFragments}, State) -> - send_method(Method, Props, BodyFragments, State). +perform_transaction_action(_, {stop, _, _} = Res) -> + Res; +perform_transaction_action({Frame, Fun}, {ok, State}) -> + process_request( + Fun, + fun(StateM) -> ensure_receipt(Frame, StateM) end, + State). %%-------------------------------------------------------------------- %% Heartbeat Management @@ -1124,29 +1497,27 @@ millis_to_seconds(M) -> M div 1000. %% Queue Setup %%---------------------------------------------------------------------------- -ensure_endpoint(_Direction, {queue, []}, _Frame, _Channel, _State) -> +ensure_endpoint(_Direction, {queue, []}, _Frame, _State) -> {error, {invalid_destination, "Destination cannot be blank"}}; -ensure_endpoint(source, EndPoint, {_, _, Headers, _} = Frame, Channel, State) -> +ensure_endpoint(source, EndPoint, {_, _, Headers, _} = Frame, State) -> Params = [{subscription_queue_name_gen, fun () -> - Id = build_subscription_id(Frame), - % Note: we discard the exchange here so there's no need to use - % the default_topic_exchange configuration key - {_, Name} = rabbit_routing_parser:parse_routing(EndPoint), - list_to_binary(rabbit_stomp_util:subscription_queue_name(Name, Id, Frame)) + Id = build_subscription_id(Frame), + % Note: we discard the exchange here so there's no need to use + % the default_topic_exchange configuration key + {_, Name} = parse_routing(EndPoint), + list_to_binary(rabbit_stomp_util:subscription_queue_name(Name, Id, Frame)) end }] ++ rabbit_stomp_util:build_params(EndPoint, Headers), Arguments = rabbit_stomp_util:build_arguments(Headers), - rabbit_routing_util:ensure_endpoint(source, Channel, EndPoint, - [Arguments | Params], State); + util_ensure_endpoint(source, EndPoint, [Arguments | Params], State); -ensure_endpoint(Direction, EndPoint, {_, _, Headers, _}, Channel, State) -> +ensure_endpoint(Direction, EndPoint, {_, _, Headers, _}, State) -> Params = rabbit_stomp_util:build_params(EndPoint, Headers), Arguments = rabbit_stomp_util:build_arguments(Headers), - rabbit_routing_util:ensure_endpoint(Direction, Channel, EndPoint, - [Arguments | Params], State). + util_ensure_endpoint(Direction, EndPoint, [Arguments | Params], State). build_subscription_id(Frame) -> case rabbit_stomp_util:has_durable_header(Frame) of @@ -1167,9 +1538,9 @@ ok(State) -> ok(Command, Headers, BodyFragments, State) -> {ok, #stomp_frame{command = Command, headers = Headers, - body_iolist = BodyFragments}, State}. + body_iolist_rev = lists:reverse(BodyFragments)}, State}. -amqp_death(access_refused = ErrorName, Explanation, State) -> +amqp_death(ErrorName, Explanation, State) when is_atom(ErrorName) -> ErrorDesc = rabbit_misc:format("~ts", [Explanation]), log_error(ErrorName, ErrorDesc, none), {stop, normal, close_connection(send_error(atom_to_list(ErrorName), ErrorDesc, State))}; @@ -1207,11 +1578,11 @@ log_error(Message, Detail, ServerPrivateDetail) -> send_frame(Command, Headers, BodyFragments, State) -> send_frame(#stomp_frame{command = Command, headers = Headers, - body_iolist = BodyFragments}, + body_iolist_rev = BodyFragments}, State). -send_frame(Frame, State = #proc_state{send_fun = SendFun, - trailing_lf = TrailingLF}) -> +send_frame(Frame, State = #state{cfg = #cfg{send_fun = SendFun, + trailing_lf = TrailingLF}}) -> SendFun(rabbit_stomp_frame:serialize(Frame, TrailingLF)), State. @@ -1220,11 +1591,11 @@ send_error_frame(Message, ExtraHeaders, Format, Args, State) -> State). send_error_frame(Message, ExtraHeaders, Detail, State) -> - send_frame("ERROR", [{"message", Message}, + send_frame('ERROR', [{"message", Message}, {"content-type", "text/plain"}, {"version", string:join(?SUPPORTED_VERSIONS, ",")}] ++ - ExtraHeaders, - Detail, State). + ExtraHeaders, + iolist_to_binary(Detail), State). send_error(Message, Detail, State) -> send_error_frame(Message, [], Detail, State). @@ -1233,12 +1604,12 @@ send_error(Message, Format, Args, State) -> send_error(Message, rabbit_misc:format(Format, Args), State). additional_info(Key, - #proc_state{adapter_info = - #amqp_adapter_info{additional_info = AddInfo}}) -> + #state{cfg = #cfg{ + adapter_info = #amqp_adapter_info{additional_info = AddInfo}}}) -> proplists:get_value(Key, AddInfo). parse_routing(Destination, DefaultTopicExchange) -> - {Exchange0, RoutingKey} = rabbit_routing_parser:parse_routing(Destination), + {Exchange0, RoutingKey} = parse_routing(Destination), Exchange1 = maybe_apply_default_topic_exchange(Exchange0, DefaultTopicExchange), {Exchange1, RoutingKey}. @@ -1255,3 +1626,478 @@ maybe_apply_default_topic_exchange(Exchange, _DefaultTopicExchange) -> %% amq.topic, so it must have been specified in the %% message headers Exchange. + +create_queue(_State = #state{authz_ctx = AuthzCtx, + user = #user{username = Username} = User, + cfg = #cfg{vhost = VHost}}) -> + QNameBin = rabbit_guid:binary(rabbit_guid:gen_secure(), "stomp.gen"), + QName = rabbit_misc:r(VHost, queue, QNameBin), + + %% configure access to queue required for queue.declare + ok = check_resource_access(User, QName, configure, AuthzCtx), + case rabbit_vhost_limit:is_over_queue_limit(VHost) of + false -> + rabbit_core_metrics:queue_declared(QName), + + case rabbit_amqqueue:declare(QName, _Durable = false, _AutoDelete = true, + [], self(), Username) of + {new, Q} when ?is_amqqueue(Q) -> + rabbit_core_metrics:queue_created(QName), + {ok, Q}; + Other -> + log_error(rabbit_misc:format("Failed to declare ~s: ~p", [rabbit_misc:rs(QName)]), Other, none), + {error, queue_declare} + end; + {true, Limit} -> + log_error(rabbit_misc:format("cannot declare ~s because ", [rabbit_misc:rs(QName)]), + rabbit_misc:format("queue limit ~p in vhost '~s' is reached", [Limit, VHost]), + none), + {error, queue_limit_exceeded} + end. + +delete_queue(QRes, Username) -> + case rabbit_amqqueue:with( + QRes, + fun (Q) -> + rabbit_queue_type:delete(Q, false, false, Username) + end, + fun (not_found) -> + ok; + ({absent, Q, crashed}) -> + rabbit_classic_queue:delete_crashed(Q, Username); + ({absent, Q, stopped}) -> + rabbit_classic_queue:delete_crashed(Q, Username); + ({absent, _Q, _Reason}) -> + ok + end) of + {ok, _N} -> + ok; + ok -> + ok + end. + +ensure_binding(#resource{name = QueueBin}, {"", Queue}, _State) -> + %% i.e., we should only be asked to bind to the default exchange a + %% queue with its own name + QueueBin = list_to_binary(Queue), + ok; +ensure_binding(QName, {Exchange, RoutingKey}, _State = #state{cfg = #cfg{ + auth_login = Username, + vhost = VHost}}) -> + Binding = #binding{source = rabbit_misc:r(VHost, exchange, list_to_binary(Exchange)), + destination = QName, + key = list_to_binary(RoutingKey)}, + Res = case rabbit_binding:add(Binding, Username) of + {error, {resources_missing, [{not_found, Name} | _]}} -> + rabbit_amqqueue:not_found(Name); + {error, {resources_missing, [{absent, Q, Reason} | _]}} -> + rabbit_amqqueue:absent(Q, Reason); + {error, {binding_invalid, Fmt, Args}} -> + rabbit_misc:protocol_error(precondition_failed, Fmt, Args); + {error, #amqp_error{} = Error} -> + rabbit_misc:protocol_error(Error); + ok -> + ok + end, + %% io:format("rabbit_binding:add ~p ~p~n", [Binding, Res]), + Res. + +check_resource_access(User, Resource, Perm, Context) -> + V = {Resource, Context, Perm}, + Cache = case get(permission_cache) of + undefined -> []; + Other -> Other + end, + case lists:member(V, Cache) of + true -> + ok; + false -> + rabbit_access_control:check_resource_access(User, Resource, Perm, Context), + CacheTail = lists:sublist(Cache, ?MAX_PERMISSION_CACHE_SIZE-1), + put(permission_cache, [V | CacheTail]), + ok + end. + +handle_down({{'DOWN', QName}, _MRef, process, QPid, Reason}, + State0 = #state{queue_states = QStates0} = State) -> + case rabbit_queue_type:handle_down(QPid, QName, Reason, QStates0) of + {ok, QStates1, Actions} -> + State1 = State0#state{queue_states = QStates1}, + State2 = handle_queue_actions(Actions, State1), + {ok, State2}; + {eol, QStates1, QRef} -> + State1 = handle_consuming_queue_down_or_eol(QRef, State#state{queue_states = QStates1}), + {ConfirmMXs, UC1} = + rabbit_confirms:remove_queue(QRef, State1#state.unconfirmed), + State2 = record_confirms(ConfirmMXs, + State1#state{unconfirmed = UC1}), + _ = erase_queue_stats(QRef), + {ok, State2#state{queue_states = rabbit_queue_type:remove(QRef, State2#state.queue_states)}} + end. + +handle_queue_event({queue_event, QRef, Evt}, #state{queue_states = QStates0} = State) -> + %% io:format("Event: ~p~n", [Evt]), + %% io:format("QStates: ~p~n", [QStates0]), + case rabbit_queue_type:handle_event(QRef, Evt, QStates0) of + {ok, QState1, Actions} -> + %% io:format("ActionsEv ~p~n", [Actions]), + State1 = State#state{queue_states = QState1}, + State2 = handle_queue_actions(Actions, State1), + {ok, State2}; + {eol, Actions} -> + State1 = handle_queue_actions(Actions, State), + State2 = handle_consuming_queue_down_or_eol(QRef, State1), + {ConfirmMXs, UC1} = + rabbit_confirms:remove_queue(QRef, State1#state.unconfirmed), + %% Deleted queue is a special case. + %% Do not nack the "rejected" messages. + State3 = record_confirms(ConfirmMXs, + State2#state{unconfirmed = UC1}), + {ok, State3#state{queue_states = rabbit_queue_type:remove(QRef, QStates0)}}; + {protocol_error, Type, Reason, ReasonArgs} = Error -> + log_error(Type, Reason, ReasonArgs), + {error, Error, State} + end. + +handle_queue_actions(Actions, #state{} = State0) -> + %% io:format("Actions: ~p~n", [Actions]), + lists:foldl( + fun ({deliver, ConsumerTag, Ack, Msgs}, S) -> + deliver_to_client(ConsumerTag, Ack, Msgs, S); + ({settled, QRef, MsgSeqNos}, S0) -> + S = confirm(MsgSeqNos, QRef, S0), + send_confirms_and_nacks(S); + ({rejected, _QRef, MsgSeqNos}, S0) -> + {U, Rej} = + lists:foldr( + fun(SeqNo, {U1, Acc}) -> + case rabbit_confirms:reject(SeqNo, U1) of + {ok, MX, U2} -> + {U2, [MX | Acc]}; + {error, not_found} -> + {U1, Acc} + end + end, {S0#state.unconfirmed, []}, MsgSeqNos), + S = S0#state{unconfirmed = U}, + %% Don't send anything, no nacks in STOMP + record_rejects(Rej, S); + ({queue_down, QRef}, S0) -> + handle_consuming_queue_down_or_eol(QRef, S0); + %% TODO: in rabbit_channel there code for handling + %% send_drained and send_credit_reply + %% I'm doing catch all here to not crash? + (_, S0) -> + S0 + end, State0, Actions). + + + +parse_endpoint(undefined) -> + parse_endpoint("/queue"); +parse_endpoint(Destination) when is_binary(Destination) -> + parse_endpoint(unicode:characters_to_list(Destination)); +parse_endpoint(Destination) when is_list(Destination) -> + case string:split(Destination, "/", all) of + [Name] -> + {ok, {queue, unescape(Name)}}; + ["", "exchange" | Rest] -> + parse_endpoint0(exchange, Rest); + ["", "queue" | Rest] -> + parse_endpoint0(queue, Rest); + ["", "topic" | Rest] -> + parse_endpoint0(topic, Rest); + ["", "temp-queue" | Rest] -> + parse_endpoint0(temp_queue, Rest); + ["", "amq", "queue" | Rest] -> + parse_endpoint0(amqqueue, Rest); + ["", "reply-queue" = Prefix | [_|_]] -> + parse_endpoint0(reply_queue, + [lists:nthtail(2 + length(Prefix), Destination)]); + _ -> + {error, {unknown_destination, Destination}} + end. + +parse_endpoint0(exchange, ["" | _] = Rest) -> + {error, {invalid_destination, exchange, to_url(Rest)}}; +parse_endpoint0(exchange, [Name]) -> + {ok, {exchange, {unescape(Name), undefined}}}; +parse_endpoint0(exchange, [Name, Pattern]) -> + {ok, {exchange, {unescape(Name), unescape(Pattern)}}}; +parse_endpoint0(queue, []) -> + {error, {invalid_destination, queue, []}}; +parse_endpoint0(Type, [[_|_]] = [Name]) -> + {ok, {Type, unescape(Name)}}; +parse_endpoint0(Type, Rest) -> + {error, {invalid_destination, Type, to_url(Rest)}}. + +%% -------------------------------------------------------------------------- + +util_ensure_endpoint(source, {exchange, {Name, _}}, Params, State = #state{cfg = #cfg{vhost = VHost}}) -> + ExchangeName = rabbit_misc:r(Name, exchange, VHost), + check_exchange(ExchangeName, proplists:get_value(check_exchange, Params, false)), + Amqqueue = new_amqqueue(undefined, exchange, Params, State), + {ok, Queue} = create_queue(Amqqueue, State), + {ok, amqqueue:get_name(Queue), State}; + +util_ensure_endpoint(source, {topic, _}, Params, State) -> + Amqqueue = new_amqqueue(undefined, topic, Params, State), + {ok, Queue} = create_queue(Amqqueue, State), + {ok, amqqueue:get_name(Queue), State}; + +util_ensure_endpoint(_Dir, {queue, undefined}, _Params, State) -> + {ok, undefined, State}; + +util_ensure_endpoint(_, {queue, Name}, Params, State=#state{route_state = RoutingState, + cfg = #cfg{vhost = VHost}}) -> + Params1 = rabbit_misc:pmerge(durable, true, Params), + QueueNameBin = list_to_binary(Name), + RState1 = case sets:is_element(QueueNameBin, RoutingState) of + true -> RoutingState; + _ -> Amqqueue = new_amqqueue(QueueNameBin, queue, Params1, State), + {ok, Queue} = create_queue(Amqqueue, State), + #resource{name = QNameBin} = amqqueue:get_name(Queue), + sets:add_element(QNameBin, RoutingState) + end, + {ok, rabbit_misc:r(VHost, queue, QueueNameBin), State#state{route_state = RState1}}; + +util_ensure_endpoint(dest, {exchange, {Name, _}}, Params, State = #state{cfg = #cfg{vhost = VHost}}) -> + ExchangeName = rabbit_misc:r(Name, exchange, VHost), + check_exchange(ExchangeName, proplists:get_value(check_exchange, Params, false)), + {ok, undefined, State}; + +util_ensure_endpoint(dest, {topic, _}, _Params, State) -> + {ok, undefined, State}; + +util_ensure_endpoint(_, {amqqueue, Name}, _Params, State = #state{cfg = #cfg{vhost = VHost}}) -> + {ok, rabbit_misc:r(VHost, queue, list_to_binary(Name)), State}; + +util_ensure_endpoint(_, {reply_queue, Name}, _Params, State = #state{cfg = #cfg{vhost = VHost}}) -> + {ok, rabbit_misc:r(VHost, queue, list_to_binary(Name)), State}; + +util_ensure_endpoint(_Direction, _Endpoint, _Params, _State) -> + {error, invalid_endpoint}. + + +%% -------------------------------------------------------------------------- + +parse_routing({exchange, {Name, undefined}}) -> + {Name, ""}; +parse_routing({exchange, {Name, Pattern}}) -> + {Name, Pattern}; +parse_routing({topic, Name}) -> + {"amq.topic", Name}; +parse_routing({Type, Name}) + when Type =:= queue orelse Type =:= reply_queue orelse Type =:= amqqueue -> + {"", Name}. + +dest_temp_queue({temp_queue, Name}) -> Name; +dest_temp_queue(_) -> none. + +%% -------------------------------------------------------------------------- + +check_exchange(_, false) -> + ok; +check_exchange(ExchangeName, true) -> + _ = rabbit_exchange:lookup_or_die(ExchangeName), + ok. + +new_amqqueue(QNameBin0, Type, Params0, _State = #state{user = #user{username = Username}, + cfg = #cfg{vhost = VHost}}) -> + QNameBin = case {Type, proplists:get_value(subscription_queue_name_gen, Params0)} of + {topic, SQNG} when is_function(SQNG) -> + SQNG(); + {exchange, SQNG} when is_function(SQNG) -> + SQNG(); + _ -> + QNameBin0 + end, + QName = rabbit_misc:r(VHost, queue, QNameBin), + %% defaults + Params = case proplists:get_value(durable, Params0, false) of + false -> [{auto_delete, true}, {exclusive, true} | Params0]; + true -> Params0 + end, + Args = proplists:get_value(arguments, Params, []), + + AMQ = amqqueue:new(QName, + none, + proplists:get_value(durable, Params, false), + proplists:get_value(auto_delete, Params, false), + case proplists:get_value(exclusive, Params, false) of + false -> none; + true -> self() + end, + Args, + VHost, + #{user => Username}, + rabbit_amqqueue:get_queue_type(Args)), + %% io:format("~p", [AMQ]), + AMQ. + + +to_url([]) -> []; +to_url(Lol) -> "/" ++ string:join(Lol, "/"). + +unescape(Str) -> unescape(Str, []). + +unescape("%2F" ++ Str, Acc) -> unescape(Str, [$/ | Acc]); +unescape([C | Str], Acc) -> unescape(Str, [C | Acc]); +unescape([], Acc) -> lists:reverse(Acc). + + +consume_queue(QRes, Spec0, State = #state{user = #user{username = Username} = User, + authz_ctx = AuthzCtx, + queue_states = QStates0})-> + check_resource_access(User, QRes, read, AuthzCtx), + Spec = Spec0#{channel_pid => self(), + limiter_pid => none, + limiter_active => false, + ok_msg => undefined, + acting_user => Username}, + rabbit_amqqueue:with_or_die( + QRes, + fun(Q1) -> + case rabbit_queue_type:consume(Q1, Spec, QStates0) of + {ok, QStates} -> + %% io:format("Consume QStates ~p ~n", [QStates]), + %% rabbit_global_counters:consumer_created('STOMP'), + State1 = State#state{queue_states = QStates}, + {ok, State1}; + {error, Reason} -> + error("Failed to consume from ~s: ~p", + [rabbit_misc:rs(QRes), Reason], + State) + end + end). + +create_queue(Amqqueue, _State = #state{authz_ctx = AuthzCtx, + user = User, + cfg = #cfg{vhost = VHost}}) -> + QName = amqqueue:get_name(Amqqueue), + + %% configure access to queue required for queue.declare + ok = check_resource_access(User, QName, configure, AuthzCtx), + + case rabbit_vhost_limit:is_over_queue_limit(VHost) of + false -> + rabbit_core_metrics:queue_declared(QName), + + case rabbit_queue_type:declare(Amqqueue, node()) of + {new, Q} when ?is_amqqueue(Q) -> + rabbit_core_metrics:queue_created(QName), + {ok, Q}; + {existing, Q} when ?is_amqqueue(Q) -> + rabbit_core_metrics:queue_created(QName), + {ok, Q}; + Other -> + log_error(rabbit_misc:format("Failed to declare ~s: ~p", [rabbit_misc:rs(QName)]), Other, none), + {error, queue_declare} + end; + {true, Limit} -> + log_error(rabbit_misc:format("cannot declare ~s because ", [rabbit_misc:rs(QName)]), + rabbit_misc:format("queue limit ~p in vhost '~s' is reached", [Limit, VHost]), + none), + {error, queue_limit_exceeded} + end. + +routing_init_state() -> sets:new([{version, 2}]). + +check_internal_exchange(#exchange{name = Name, internal = true}) -> + rabbit_misc:protocol_error(access_refused, + "cannot publish to internal ~ts", + [rabbit_misc:rs(Name)]); +check_internal_exchange(_) -> + ok. + + +check_topic_authorisation(#exchange{name = Name = #resource{virtual_host = VHost}, type = topic}, + User = #user{username = Username}, + RoutingKey, AuthzContext, Permission) -> + Resource = Name#resource{kind = topic}, + VariableMap = build_topic_variable_map(AuthzContext, VHost, Username), + Context = #{routing_key => RoutingKey, + variable_map => VariableMap}, + Cache = case get(topic_permission_cache) of + undefined -> []; + Other -> Other + end, + case lists:member({Resource, Context, Permission}, Cache) of + true -> ok; + false -> ok = rabbit_access_control:check_topic_access( + User, Resource, Permission, Context), + CacheTail = lists:sublist(Cache, ?MAX_PERMISSION_CACHE_SIZE-1), + put(topic_permission_cache, [{Resource, Context, Permission} | CacheTail]) + end; +check_topic_authorisation(_, _, _, _, _) -> + ok. + + +build_topic_variable_map(AuthzContext, VHost, Username) when is_map(AuthzContext) -> + maps:merge(AuthzContext, #{<<"vhost">> => VHost, <<"username">> => Username}); +build_topic_variable_map(AuthzContext, VHost, Username) -> + maps:merge(extract_variable_map_from_amqp_params(AuthzContext), #{<<"vhost">> => VHost, <<"username">> => Username}). + +%% Use tuple representation of amqp_params to avoid a dependency on amqp_client. +%% Extracts variable map only from amqp_params_direct, not amqp_params_network. +%% amqp_params_direct records are usually used by plugins (e.g. STOMP) +extract_variable_map_from_amqp_params({amqp_params, {amqp_params_direct, _, _, _, _, + {amqp_adapter_info, _,_,_,_,_,_,AdditionalInfo}, _}}) -> + proplists:get_value(variable_map, AdditionalInfo, #{}); +extract_variable_map_from_amqp_params({amqp_params_direct, _, _, _, _, + {amqp_adapter_info, _,_,_,_,_,_,AdditionalInfo}, _}) -> + proplists:get_value(variable_map, AdditionalInfo, #{}); +extract_variable_map_from_amqp_params([Value]) -> + extract_variable_map_from_amqp_params(Value); +extract_variable_map_from_amqp_params(_) -> + #{}. + +check_vhost_exists(VHost, Username, PeerIp) -> + case rabbit_vhost:exists(VHost) of + true -> + ok; + false -> + rabbit_core_metrics:auth_attempt_failed(PeerIp, Username, stomp), + ?LOG_ERROR("STOMP connection failed: virtual host '~s' does not exist", [VHost]), + {error, not_allowed} + end. + +check_vhost_access(VHost, User = #user{username = Username}, PeerIp) -> + AuthzCtx = #{}, + try rabbit_access_control:check_vhost_access( + User, VHost, {ip, PeerIp}, AuthzCtx) of + ok -> + {ok, AuthzCtx} + catch exit:#amqp_error{name = not_allowed} -> + rabbit_core_metrics:auth_attempt_failed(PeerIp, Username, stomp), + ?LOG_ERROR("STOMP connection failed: access refused for user '~s' to vhost '~s'", + [Username, VHost]), + {error, not_allowed} + end. + +check_vhost_connection_limit(VHost) -> + case rabbit_vhost_limit:is_over_connection_limit(VHost) of + false -> + ok; + {true, Limit} -> + ?LOG_ERROR("STOMP connection failed: connection limit ~p is reached for vhost '~s'", + [Limit, VHost]), + {error, quota_exceeded} + end. + +check_user_loopback(Username, PeerIp) -> + case rabbit_access_control:check_user_loopback(Username, PeerIp) of + ok -> + ok; + not_allowed -> + rabbit_core_metrics:auth_attempt_failed(PeerIp, Username, stomp), + {error, not_loopback} + end. + +erase_queue_stats(QName) -> + rabbit_core_metrics:channel_queue_down({self(), QName}), + erase({queue_stats, QName}), + [begin + rabbit_core_metrics:channel_queue_exchange_down({self(), QX}), + erase({queue_exchange_stats, QX}) + end || {{queue_exchange_stats, QX = {QName0, _}}, _} <- get(), + QName0 =:= QName]. diff --git a/deps/rabbitmq_stomp/src/rabbit_stomp_reader.erl b/deps/rabbitmq_stomp/src/rabbit_stomp_reader.erl index 7f377f45b1ff..3060820b9fa7 100644 --- a/deps/rabbitmq_stomp/src/rabbit_stomp_reader.erl +++ b/deps/rabbitmq_stomp/src/rabbit_stomp_reader.erl @@ -18,11 +18,6 @@ -include("rabbit_stomp.hrl"). -include("rabbit_stomp_frame.hrl"). --include_lib("amqp_client/include/amqp_client.hrl"). - --define(SIMPLE_METRICS, [pid, recv_oct, send_oct, reductions]). --define(OTHER_METRICS, [recv_cnt, send_cnt, send_pend, garbage_collection, state, - timeout]). -record(reader_state, { socket, @@ -40,7 +35,8 @@ heartbeat_sup, heartbeat, %% heartbeat timeout value used, 0 means %% heartbeats are disabled - timeout_sec + timeout_sec, + parser_config }). %%---------------------------------------------------------------------------- @@ -77,19 +73,27 @@ init([SupHelperPid, Ref, Configuration]) -> rabbit_log_connection:info("accepting STOMP connection ~tp (~ts)", [self(), ConnName]), - ParseState = rabbit_stomp_frame:initial_state(), + ParserConfig = #stomp_parser_config{ + max_headers = Configuration#stomp_configuration.max_headers, + max_header_length = Configuration#stomp_configuration.max_header_length, + max_body_length = Configuration#stomp_configuration.max_body_length + }, + ParseState = rabbit_stomp_frame:initial_state(ParserConfig), _ = register_resource_alarm(), LoginTimeout = application:get_env(rabbitmq_stomp, login_timeout, 10_000), MaxFrameSize = application:get_env(rabbitmq_stomp, max_frame_size, ?DEFAULT_MAX_FRAME_SIZE), erlang:send_after(LoginTimeout, self(), login_timeout), + rabbit_networking:register_non_amqp_connection(self()), + gen_server2:enter_loop(?MODULE, [], rabbit_event:init_stats_timer( run_socket(control_throttle( #reader_state{socket = RealSocket, conn_name = ConnName, parse_state = ParseState, + parser_config = ParserConfig, processor_state = ProcState, heartbeat_sup = SupHelperPid, heartbeat = {none, none}, @@ -118,6 +122,14 @@ handle_call({info, InfoItems}, _From, State) -> handle_call(Msg, From, State) -> {stop, {stomp_unexpected_call, Msg, From}, State}. +handle_cast(QueueEvent = {queue_event, _, _}, State) -> + ProcState = processor_state(State), + case rabbit_stomp_processor:handle_queue_event(QueueEvent, ProcState) of + {ok, NewProcState} -> + {noreply, processor_state(NewProcState, State), hibernate}; + {error, Reason, NewProcState} -> + {stop, Reason, processor_state(NewProcState, State)} + end; handle_cast({close_connection, Reason}, State) -> {stop, {shutdown, {server_initiated_close, Reason}}, State}; handle_cast(client_timeout, State) -> @@ -125,6 +137,11 @@ handle_cast(client_timeout, State) -> handle_cast(Msg, State) -> {stop, {stomp_unexpected_cast, Msg}, State}. +handle_info(connection_created, State) -> + Infos = infos(?INFO_ITEMS ++ ?OTHER_METRICS, State), + rabbit_core_metrics:connection_created(self(), Infos), + rabbit_event:notify(connection_created, Infos), + {noreply, State, hibernate}; handle_info({Tag, Sock, Data}, State=#reader_state{socket=Sock}) when Tag =:= tcp; Tag =:= ssl -> @@ -149,6 +166,15 @@ handle_info({bump_credit, Msg}, State) -> credit_flow:handle_bump_msg(Msg), {noreply, run_socket(control_throttle(State)), hibernate}; +handle_info({{'DOWN', _QName}, _MRef, process, _Pid, _Reason} = Evt, State) -> + ProcState = processor_state(State), + {ok, NewProcState} = rabbit_stomp_processor:handle_down(Evt, ProcState), + {noreply, processor_state(NewProcState, State), hibernate}; + +handle_info({'DOWN', _MRef, process, QPid, _Reason}, State) -> + rabbit_amqqueue_common:notify_sent_queue_down(QPid), + {noreply, State, hibernate}; + %%---------------------------------------------------------------------------- handle_info(client_timeout, State) -> @@ -156,8 +182,8 @@ handle_info(client_timeout, State) -> handle_info(login_timeout, State) -> ProcState = processor_state(State), - case rabbit_stomp_processor:info(channel, ProcState) of - none -> + case rabbit_stomp_processor:info(user, ProcState) of + undefined -> {stop, {shutdown, login_timeout}, State}; _ -> {noreply, State, hibernate} @@ -165,42 +191,6 @@ handle_info(login_timeout, State) -> %%---------------------------------------------------------------------------- -handle_info(#'basic.consume_ok'{}, State) -> - {noreply, State, hibernate}; -handle_info(#'basic.cancel_ok'{}, State) -> - {noreply, State, hibernate}; -handle_info(#'basic.ack'{delivery_tag = Tag, multiple = IsMulti}, State) -> - ProcState = processor_state(State), - NewProcState = rabbit_stomp_processor:flush_pending_receipts(Tag, - IsMulti, - ProcState), - {noreply, processor_state(NewProcState, State), hibernate}; -handle_info({Delivery = #'basic.deliver'{}, - Message = #amqp_msg{}}, - State) -> - %% receiving a message from a quorum queue - %% no delivery context - handle_info({Delivery, Message, undefined}, State); -handle_info({Delivery = #'basic.deliver'{}, - #amqp_msg{props = Props, payload = Payload}, - DeliveryCtx}, - State) -> - ProcState = processor_state(State), - NewProcState = rabbit_stomp_processor:send_delivery(Delivery, - Props, - Payload, - DeliveryCtx, - ProcState), - {noreply, processor_state(NewProcState, State), hibernate}; -handle_info(#'basic.cancel'{consumer_tag = Ctag}, State) -> - ProcState = processor_state(State), - case rabbit_stomp_processor:cancel_consumer(Ctag, ProcState) of - {ok, NewProcState, _} -> - {noreply, processor_state(NewProcState, State), hibernate}; - {stop, Reason, NewProcState} -> - {stop, Reason, processor_state(NewProcState, State)} - end; - handle_info({start_heartbeats, {0, 0}}, State) -> {noreply, State#reader_state{timeout_sec = {0, 0}}}; @@ -217,14 +207,8 @@ handle_info({start_heartbeats, {SendTimeout, ReceiveTimeout}}, %%---------------------------------------------------------------------------- -handle_info({'EXIT', From, Reason}, State) -> - ProcState = processor_state(State), - case rabbit_stomp_processor:handle_exit(From, Reason, ProcState) of - {stop, NewReason, NewProcState} -> - {stop, NewReason, processor_state(NewProcState, State)}; - unknown_exit -> - {stop, {connection_died, Reason}, State} - end. +handle_info({'EXIT', _From, Reason}, State) -> + {stop, {connection_died, Reason}, State}. %%---------------------------------------------------------------------------- process_received_bytes([], State) -> @@ -254,14 +238,13 @@ process_received_bytes(Bytes, {stop, normal, State}; false -> try rabbit_stomp_processor:process_frame(Frame, ProcState) of - {ok, NewProcState, Conn} -> + {ok, NewProcState} -> PS = rabbit_stomp_frame:initial_state(), NextState = maybe_block(State, Frame), process_received_bytes(Rest, NextState#reader_state{ current_frame_size = 0, processor_state = NewProcState, - parse_state = PS, - connection = Conn}); + parse_state = PS}); {stop, Reason, NewProcState} -> {stop, Reason, processor_state(NewProcState, State)} @@ -305,7 +288,7 @@ control_throttle(State = #reader_state{state = CS, end. maybe_block(State = #reader_state{state = blocking, heartbeat = Heartbeat}, - #stomp_frame{command = "SEND"}) -> + #stomp_frame{command = 'SEND'}) -> rabbit_heartbeat:pause_monitor(Heartbeat), State#reader_state{state = blocked}; maybe_block(State, _) -> @@ -324,9 +307,13 @@ terminate(Reason, undefined) -> log_reason(Reason, undefined), {stop, Reason}; terminate(Reason, State = #reader_state{processor_state = ProcState}) -> - maybe_emit_stats(State), - log_reason(Reason, State), - _ = rabbit_stomp_processor:flush_and_die(ProcState), + maybe_emit_stats(State), + rabbit_core_metrics:connection_closed(self()), + Infos = infos(?OTHER_METRICS, State), + rabbit_event:notify(connection_closed, Infos), + rabbit_networking:unregister_non_amqp_connection(self()), + log_reason(Reason, State), + _ = rabbit_stomp_processor:flush_and_die(ProcState), {stop, Reason}. code_change(_OldVsn, State, _Extra) -> @@ -415,6 +402,20 @@ processor_args(Configuration, Sock) -> ssl_login_name(RealSocket, Configuration), PeerAddr}. adapter_info(Sock) -> +%% case rabbit_net:socket_ends(Socket, inbound) of +%% {ok, {PeerIp, PeerPort, Ip, Port}} -> +%% #amqp_adapter_info{protocol = {'STOMP', 0}, +%% name = Name, +%% host = Host, +%% port = Port, +%% peer_host = PeerHost, +%% peer_port = PeerPort, +%% additional_info = maybe_ssl_info(Sock)} +%% process_connect(ConnectPacket, Socket, ConnName, SendFun, SocketEnds); +%% {error, Reason} -> +%% {error, {socket_ends, Reason}} +%% end. + amqp_connection:socket_adapter_info(Sock, {'STOMP', 0}). ssl_login_name(_Sock, #stomp_configuration{ssl_cert_login = false}) -> @@ -439,11 +440,11 @@ maybe_emit_stats(State) -> rabbit_event:if_enabled(State, #reader_state.stats_timer, fun() -> emit_stats(State) end). -emit_stats(State=#reader_state{connection = C}) when C == none; C == undefined -> - %% Avoid emitting stats on terminate when the connection has not yet been - %% established, as this causes orphan entries on the stats database - State1 = rabbit_event:reset_stats_timer(State, #reader_state.stats_timer), - ensure_stats_timer(State1); +%% emit_stats(State=#reader_state{connection = C}) when C == none; C == undefined -> +%% %% Avoid emitting stats on terminate when the connection has not yet been +%% %% established, as this causes orphan entries on the stats database +%% State1 = rabbit_event:reset_stats_timer(State, #reader_state.stats_timer), +%% ensure_stats_timer(State1); emit_stats(State) -> [{_, Pid}, {_, Recv_oct}, @@ -469,7 +470,7 @@ processor_state(ProcState, #reader_state{} = State) -> infos(Items, State) -> [{Item, info_internal(Item, State)} || Item <- Items]. -info_internal(pid, State) -> info_internal(connection, State); +info_internal(pid, _) -> self(); info_internal(SockStat, #reader_state{socket = Sock}) when SockStat =:= recv_oct; SockStat =:= recv_cnt; SockStat =:= send_oct; @@ -491,8 +492,10 @@ info_internal(timeout, #reader_state{timeout_sec = undefined}) -> 0; info_internal(conn_name, #reader_state{conn_name = Val}) -> rabbit_data_coercion:to_binary(Val); -info_internal(connection, #reader_state{connection = Val}) -> - Val; +info_internal(name, #reader_state{conn_name = Val}) -> + rabbit_data_coercion:to_binary(Val); +info_internal(connection, #reader_state{connection = _Val}) -> + self(); info_internal(connection_state, #reader_state{state = Val}) -> Val; info_internal(Key, #reader_state{processor_state = ProcState}) -> diff --git a/deps/rabbitmq_stomp/src/rabbit_stomp_util.erl b/deps/rabbitmq_stomp/src/rabbit_stomp_util.erl index e826dd3533c9..212a9e79d444 100644 --- a/deps/rabbitmq_stomp/src/rabbit_stomp_util.erl +++ b/deps/rabbitmq_stomp/src/rabbit_stomp_util.erl @@ -17,7 +17,6 @@ -export([trim_headers/1]). -include_lib("amqp_client/include/amqp_client.hrl"). --include_lib("amqp_client/include/rabbit_routing_prefixes.hrl"). -include("rabbit_stomp_frame.hrl"). -include("rabbit_stomp_headers.hrl"). @@ -135,10 +134,9 @@ headers_extra(SessionId, AckMode, Version, end. headers_post_process(Headers) -> - Prefixes = rabbit_routing_util:dest_prefixes(), [case Header of {?HEADER_REPLY_TO, V} -> - case lists:any(fun (P) -> lists:prefix(P, V) end, Prefixes) of + case lists:any(fun (P) -> lists:prefix(P, V) end, ?DEST_PREFIXES) of true -> {?HEADER_REPLY_TO, V}; false -> {?HEADER_REPLY_TO, ?REPLY_QUEUE_PREFIX ++ V} end; diff --git a/deps/rabbitmq_stomp/test/command_SUITE.erl b/deps/rabbitmq_stomp/test/command_SUITE.erl index ce54cac88a9f..ac982c514b9e 100644 --- a/deps/rabbitmq_stomp/test/command_SUITE.erl +++ b/deps/rabbitmq_stomp/test/command_SUITE.erl @@ -95,7 +95,7 @@ run(Config) -> start_amqp_connection(direct, Node, Port), - %% Still two MQTT connections, one direct AMQP 0-9-1 connection + %% Still two STOMP connections, one direct AMQP 0-9-1 connection [[{session_id, _}], [{session_id, _}]] = 'Elixir.Enum':to_list(?COMMAND:run([<<"session_id">>], Opts)), diff --git a/deps/rabbitmq_stomp/test/connections_SUITE.erl b/deps/rabbitmq_stomp/test/connections_SUITE.erl index 105425856ff3..f78f7d4ff43c 100644 --- a/deps/rabbitmq_stomp/test/connections_SUITE.erl +++ b/deps/rabbitmq_stomp/test/connections_SUITE.erl @@ -101,7 +101,7 @@ messages_not_dropped_on_disconnect(Config) -> N1 = N + 1, N1 = count_connections(Config), [rabbit_stomp_client:send( - Client, "SEND", [{"destination", ?DESTINATION}], + Client, 'SEND', [{"destination", ?DESTINATION}], [integer_to_list(Count)]) || Count <- lists:seq(1, 1000)], rabbit_stomp_client:disconnect(Client), QName = rabbit_misc:r(<<"/">>, queue, <<"bulk-test">>), @@ -184,7 +184,7 @@ frame_size(Config) -> {ok, Client} = rabbit_stomp_client:connect("1.2", "guest", "guest", StompPort, [{"heart-beat", "5000,7000"}]), ok = rabbit_stomp_client:send( - Client, "SEND", [{"destination", "qwe"}], + Client, 'SEND', [{"destination", "qwe"}], ["Lorem ipsum dolor sit amet viverra fusce. " "Lorem ipsum dolor sit amet viverra fusce. " "Lorem ipsum dolor sit amet viverra fusce." @@ -203,7 +203,7 @@ frame_size_huge(Config) -> {ok, Client} = rabbit_stomp_client:connect("1.2", "guest", "guest", StompPort, [{"heart-beat", "5000,7000"}]), rabbit_stomp_client:send( - Client, "SEND", [{"destination", "qwe"}], + Client, 'SEND', [{"destination", "qwe"}], [base64:encode(crypto:strong_rand_bytes(100000000))]), {S, _} = Client, {error, closed} = gen_tcp:recv(S, 0, 500), diff --git a/deps/rabbitmq_stomp/test/frame_SUITE.erl b/deps/rabbitmq_stomp/test/frame_SUITE.erl index 847b25a02043..c0ab8e9e2891 100644 --- a/deps/rabbitmq_stomp/test/frame_SUITE.erl +++ b/deps/rabbitmq_stomp/test/frame_SUITE.erl @@ -25,6 +25,8 @@ all() -> parse_carriage_return_not_ignored_interframe, parse_carriage_return_mid_command, parse_carriage_return_end_command, + parse_unknown_command, + parse_unknown_command_short, parse_resume_mid_command, parse_resume_mid_header_key, parse_resume_mid_header_val, @@ -50,87 +52,93 @@ parse_simple_frame_crlf(_) -> parse_simple_frame_gen(Term) -> Headers = [{"header1", "value1"}, {"header2", "value2"}], - Content = frame_string("COMMAND", + Content = frame_string('CONNECT', Headers, "Body Content", Term), - {"COMMAND", Frame, _State} = parse_complete(Content), + {'CONNECT', Frame, _State} = parse_complete(Content), [?assertEqual({ok, Value}, rabbit_stomp_frame:header(Frame, Key)) || {Key, Value} <- Headers], - #stomp_frame{body_iolist = Body} = Frame, + #stomp_frame{body_iolist_rev = Body} = Frame, ?assertEqual(<<"Body Content">>, iolist_to_binary(Body)). parse_command_only(_) -> - {ok, #stomp_frame{command = "COMMAND"}, _Rest} = parse("COMMAND\n\n\0"). + {ok, #stomp_frame{command = "CONNECT"}, _Rest} = parse("CONNECT\n\n\0"). parse_command_prefixed_with_newline(_) -> - {ok, #stomp_frame{command = "COMMAND"}, _Rest} = parse("\nCOMMAND\n\n\0"). + {ok, #stomp_frame{command = "CONNECT"}, _Rest} = parse("\nCONNECT\n\n\0"). parse_ignore_empty_frames(_) -> - {ok, #stomp_frame{command = "COMMAND"}, _Rest} = parse("\0\0COMMAND\n\n\0"). + {ok, #stomp_frame{command = "CONNECT"}, _Rest} = parse("\0\0CONNECT\n\n\0"). parse_heartbeat_interframe(_) -> - {ok, #stomp_frame{command = "COMMAND"}, _Rest} = parse("\nCOMMAND\n\n\0"). + {ok, #stomp_frame{command = "CONNECT"}, _Rest} = parse("\nCONNECT\n\n\0"). parse_crlf_interframe(_) -> - {ok, #stomp_frame{command = "COMMAND"}, _Rest} = parse("\r\nCOMMAND\n\n\0"). + {ok, #stomp_frame{command = "CONNECT"}, _Rest} = parse("\r\nCONNECT\n\n\0"). parse_carriage_return_not_ignored_interframe(_) -> - {error, {unexpected_chars_between_frames, "\rC"}} = parse("\rCOMMAND\n\n\0"). + {error, {unexpected_chars_between_frames, "\rC"}} = parse("\rCONNECT\n\n\0"). parse_carriage_return_mid_command(_) -> {error, {unexpected_chars_in_command, "\rA"}} = parse("COMM\rAND\n\n\0"). parse_carriage_return_end_command(_) -> - {error, {unexpected_chars_in_command, "\r\r"}} = parse("COMMAND\r\r\n\n\0"). + {error, {unexpected_chars_in_command, "\r\r"}} = parse("CONNECT\r\r\n\n\0"). + +parse_unknown_command(_) -> + {error, unknown_command} = parse("CONNECTA\r\r\n\n\0"). + +parse_unknown_command_short(_) -> + {error, unknown_command} = parse("CONNE\n\n\0"). parse_resume_mid_command(_) -> - First = "COMM", - Second = "AND\n\n\0", + First = "CONN", + Second = "ECT\n\n\0", {more, Resume} = parse(First), - {ok, #stomp_frame{command = "COMMAND"}, _Rest} = parse(Second, Resume). + {ok, #stomp_frame{command = 'CONNECT'}, _Rest} = parse(Second, Resume). parse_resume_mid_header_key(_) -> - First = "COMMAND\nheade", + First = "CONNECT\nheade", Second = "r1:value1\n\n\0", {more, Resume} = parse(First), - {ok, Frame = #stomp_frame{command = "COMMAND"}, _Rest} = + {ok, Frame = #stomp_frame{command = 'CONNECT'}, _Rest} = parse(Second, Resume), ?assertEqual({ok, "value1"}, rabbit_stomp_frame:header(Frame, "header1")). parse_resume_mid_header_val(_) -> - First = "COMMAND\nheader1:val", + First = "CONNECT\nheader1:val", Second = "ue1\n\n\0", {more, Resume} = parse(First), - {ok, Frame = #stomp_frame{command = "COMMAND"}, _Rest} = + {ok, Frame = #stomp_frame{command = 'CONNECT'}, _Rest} = parse(Second, Resume), ?assertEqual({ok, "value1"}, rabbit_stomp_frame:header(Frame, "header1")). parse_resume_mid_body(_) -> - First = "COMMAND\n\nABC", + First = "CONNECT\n\nABC", Second = "DEF\0", {more, Resume} = parse(First), - {ok, #stomp_frame{command = "COMMAND", body_iolist = Body}, _Rest} = + {ok, #stomp_frame{command = 'CONNECT', body_iolist_rev = Body}, _Rest} = parse(Second, Resume), ?assertEqual([<<"ABC">>, <<"DEF">>], Body). parse_no_header_stripping(_) -> - Content = "COMMAND\nheader: foo \n\n\0", + Content = "CONNECT\nheader: foo \n\n\0", {ok, Frame, _} = parse(Content), {ok, Val} = rabbit_stomp_frame:header(Frame, "header"), ?assertEqual(" foo ", Val). parse_multiple_headers(_) -> - Content = "COMMAND\nheader:correct\nheader:incorrect\n\n\0", + Content = "CONNECT\nheader:correct\nheader:incorrect\n\n\0", {ok, Frame, _} = parse(Content), {ok, Val} = rabbit_stomp_frame:header(Frame, "header"), ?assertEqual("correct", Val). header_no_colon(_) -> - Content = "COMMAND\n" + Content = "CONNECT\n" "hdr1:val1\n" "hdrerror\n" "hdr2:val2\n" @@ -138,30 +146,30 @@ header_no_colon(_) -> ?assertEqual(parse(Content), {error, {header_no_value, "hdrerror"}}). no_nested_escapes(_) -> - Content = "COM\\\\rAND\n" % no escapes + Content = "CONNECT\n" % no escapes "hdr\\\\rname:" % one escape "hdr\\\\rval\n\n\0", % one escape {ok, Frame, _} = parse(Content), ?assertEqual(Frame, - #stomp_frame{command = "COM\\\\rAND", + #stomp_frame{command = 'CONNECT', headers = [{"hdr\\rname", "hdr\\rval"}], - body_iolist = []}). + body_iolist_rev = []}). header_name_with_cr(_) -> - Content = "COMMAND\nhead\rer:val\n\n\0", + Content = "CONNECT\nhead\rer:val\n\n\0", {error, {unexpected_chars_in_header, "\re"}} = parse(Content). header_value_with_cr(_) -> - Content = "COMMAND\nheader:val\rue\n\n\0", + Content = "CONNECT\nheader:val\rue\n\n\0", {error, {unexpected_chars_in_header, "\ru"}} = parse(Content). header_value_with_colon(_) -> - Content = "COMMAND\nheader:val:ue\n\n\0", + Content = "CONNECT\nheader:val:ue\n\n\0", {ok, Frame, _} = parse(Content), ?assertEqual(Frame, - #stomp_frame{ command = "COMMAND", + #stomp_frame{ command = 'CONNECT', headers = [{"header", "val:ue"}], - body_iolist = []}). + body_iolist_rev = []}). stream_offset_header(_) -> TestCases = [ @@ -207,10 +215,10 @@ test_frame_serialization(Expected, TrailingLF) -> ?assertEqual(Expected, rabbit_misc:format("~ts", [Serialized])). headers_escaping_roundtrip(_) -> - test_frame_serialization("COMMAND\nhead\\r\\c\\ner:\\c\\n\\r\\\\\n\n\0\n", true). + test_frame_serialization("CONNECT\nhead\\r\\c\\ner:\\c\\n\\r\\\\\n\n\0\n", true). headers_escaping_roundtrip_without_trailing_lf(_) -> - test_frame_serialization("COMMAND\nhead\\r\\c\\ner:\\c\\n\\r\\\\\n\n\0", false). + test_frame_serialization("CONNECT\nhead\\r\\c\\ner:\\c\\n\\r\\\\\n\n\0", false). parse(Content) -> parse(Content, rabbit_stomp_frame:initial_state()). diff --git a/deps/rabbitmq_stomp/test/proxy_protocol_SUITE.erl b/deps/rabbitmq_stomp/test/proxy_protocol_SUITE.erl index 252c453a9daa..3484b228b1ac 100644 --- a/deps/rabbitmq_stomp/test/proxy_protocol_SUITE.erl +++ b/deps/rabbitmq_stomp/test/proxy_protocol_SUITE.erl @@ -108,7 +108,7 @@ proxy_protocol_v2_local(Config) -> connection_name() -> Connections = ets:tab2list(connection_created), {_Key, Values} = lists:nth(1, Connections), - {_, Name} = lists:keyfind(name, 1, Values), + {_, Name} = lists:keyfind(conn_name, 1, Values), Name. merge_app_env(StompConfig, Config) -> diff --git a/deps/rabbitmq_stomp/test/python_SUITE.erl b/deps/rabbitmq_stomp/test/python_SUITE.erl index 1bf713d88a6f..b650319f25f0 100644 --- a/deps/rabbitmq_stomp/test/python_SUITE.erl +++ b/deps/rabbitmq_stomp/test/python_SUITE.erl @@ -31,13 +31,11 @@ groups() -> ]. init_per_suite(Config) -> - DataDir = ?config(data_dir, Config), {ok, _} = rabbit_ct_helpers:exec(["pip", "install", "-r", requirements_path(Config), - "--target", deps_path(Config)]), + "--target", deps_path(Config)]), Config. end_per_suite(Config) -> - DataDir = ?config(data_dir, Config), ok = file:del_dir_r(deps_path(Config)), Config. @@ -82,8 +80,10 @@ run(Config, Test) -> StompPort = rabbit_ct_broker_helpers:get_node_config(Config, 0, tcp_port_stomp), StompPortTls = rabbit_ct_broker_helpers:get_node_config(Config, 0, tcp_port_stomp_tls), AmqpPort = rabbit_ct_broker_helpers:get_node_config(Config, 0, tcp_port_amqp), + MgmtPort = rabbit_ct_broker_helpers:get_node_config(Config, 0, tcp_port_mgmt), NodeName = rabbit_ct_broker_helpers:get_node_config(Config, 0, nodename), os:putenv("AMQP_PORT", integer_to_list(AmqpPort)), + os:putenv("MGMT_PORT", integer_to_list(MgmtPort)), os:putenv("STOMP_PORT", integer_to_list(StompPort)), os:putenv("STOMP_PORT_TLS", integer_to_list(StompPortTls)), os:putenv("RABBITMQ_NODENAME", atom_to_list(NodeName)), diff --git a/deps/rabbitmq_stomp/test/python_SUITE_data/src/destinations.py b/deps/rabbitmq_stomp/test/python_SUITE_data/src/destinations.py index 4730c02dac5d..962385602ab4 100644 --- a/deps/rabbitmq_stomp/test/python_SUITE_data/src/destinations.py +++ b/deps/rabbitmq_stomp/test/python_SUITE_data/src/destinations.py @@ -37,7 +37,7 @@ def test_invalid_exchange(self): self.assertListener("Expecting an error", numErrs=1) err = self.listener.errors[0] self.assertEqual("not_found", err['headers']['message']) - self.assertRegex(err['message'], r'^NOT_FOUND') + self.assertRegex(err['message'], r'^no exchange') time.sleep(1) self.assertFalse(self.conn.is_connected()) @@ -489,4 +489,4 @@ def test_durable_subscribe_no_id_and_legacy_header(self): modules = [ __name__ ] - test_runner.run_unittests(modules) \ No newline at end of file + test_runner.run_unittests(modules) diff --git a/deps/rabbitmq_stomp/test/python_SUITE_data/src/redelivered.py b/deps/rabbitmq_stomp/test/python_SUITE_data/src/redelivered.py index 841b849969f0..5a6dd5466f36 100644 --- a/deps/rabbitmq_stomp/test/python_SUITE_data/src/redelivered.py +++ b/deps/rabbitmq_stomp/test/python_SUITE_data/src/redelivered.py @@ -36,6 +36,8 @@ def test_redelivered(self): self.assertTrue(listener2.wait(), "message not received again") self.assertEqual(1, len(listener2.messages)) self.assertEqual('true', listener2.messages[0]['headers']['redelivered']) + mid = listener2.messages[0]['headers'][self.ack_id_source_header] + self.ack_message(conn2, mid, None) finally: conn2.disconnect() @@ -44,4 +46,4 @@ def test_redelivered(self): modules = [ __name__ ] - test_runner.run_unittests(modules) \ No newline at end of file + test_runner.run_unittests(modules) diff --git a/deps/rabbitmq_stomp/test/python_SUITE_data/src/requirements.txt b/deps/rabbitmq_stomp/test/python_SUITE_data/src/requirements.txt index b87f22609323..fd2cc9d6beb1 100644 --- a/deps/rabbitmq_stomp/test/python_SUITE_data/src/requirements.txt +++ b/deps/rabbitmq_stomp/test/python_SUITE_data/src/requirements.txt @@ -1,3 +1,3 @@ stomp.py==8.1.0 pika==1.1.0 - +rabbitman===0.1.0 diff --git a/deps/rabbitmq_stomp/test/python_SUITE_data/src/topic_permissions.py b/deps/rabbitmq_stomp/test/python_SUITE_data/src/topic_permissions.py index e6ae5f14cf1a..d6749c1680c7 100644 --- a/deps/rabbitmq_stomp/test/python_SUITE_data/src/topic_permissions.py +++ b/deps/rabbitmq_stomp/test/python_SUITE_data/src/topic_permissions.py @@ -49,7 +49,7 @@ def test_publish_authorisation(self): # assert errors self.assertGreater(len(self.listener.errors), 0) - self.assertIn("ACCESS_REFUSED", self.listener.errors[0]['message']) + self.assertIn("access_refused", self.listener.errors[0]['headers']['message']) if __name__ == '__main__': @@ -57,4 +57,4 @@ def test_publish_authorisation(self): modules = [ __name__ ] - test_runner.run_unittests(modules) \ No newline at end of file + test_runner.run_unittests(modules) diff --git a/deps/rabbitmq_stomp/test/python_SUITE_data/src/x_queue_type_quorum.py b/deps/rabbitmq_stomp/test/python_SUITE_data/src/x_queue_type_quorum.py index a1dc7d477e26..ddf89b884a52 100644 --- a/deps/rabbitmq_stomp/test/python_SUITE_data/src/x_queue_type_quorum.py +++ b/deps/rabbitmq_stomp/test/python_SUITE_data/src/x_queue_type_quorum.py @@ -10,7 +10,7 @@ import time import os import re - +import rabbitman class TestUserGeneratedQueueName(base.BaseTest): @@ -34,6 +34,11 @@ def test_quorum_queue(self): # let the quorum queue some time to start time.sleep(5) + client = rabbitman.Client(f'http://localhost:{(os.environ["MGMT_PORT"])}', 'guest', 'guest') + queue = client.get_queues_by_vhost_and_name("/", queueName) + + self.assertEqual(queue['type'], 'quorum') + connection = pika.BlockingConnection( pika.ConnectionParameters(host='127.0.0.1', port=int(os.environ["AMQP_PORT"]))) channel = connection.channel() diff --git a/deps/rabbitmq_stomp/test/python_SUITE_data/src/x_queue_type_stream.py b/deps/rabbitmq_stomp/test/python_SUITE_data/src/x_queue_type_stream.py index af5a6e2ca37b..7a8073ec4397 100644 --- a/deps/rabbitmq_stomp/test/python_SUITE_data/src/x_queue_type_stream.py +++ b/deps/rabbitmq_stomp/test/python_SUITE_data/src/x_queue_type_stream.py @@ -10,7 +10,7 @@ import time import os import re -import urllib.request, json +import rabbitman class TestUserGeneratedQueueName(base.BaseTest): @@ -34,12 +34,17 @@ def test_stream_queue(self): 'id': 1234, 'prefetch-count': 10 }, - ack="client" + ack="client" ) # let the stream queue some time to start time.sleep(5) + client = rabbitman.Client(f'http://localhost:{(os.environ["MGMT_PORT"])}', 'guest', 'guest') + queue = client.get_queues_by_vhost_and_name("/", queueName) + + self.assertEqual(queue['type'], 'stream') + connection = pika.BlockingConnection( pika.ConnectionParameters(host='127.0.0.1', port=int(os.environ["AMQP_PORT"]))) channel = connection.channel() diff --git a/deps/rabbitmq_stomp/test/src/rabbit_stomp_client.erl b/deps/rabbitmq_stomp/test/src/rabbit_stomp_client.erl index 5849e7fd9cd4..faa140e2e4f8 100644 --- a/deps/rabbitmq_stomp/test/src/rabbit_stomp_client.erl +++ b/deps/rabbitmq_stomp/test/src/rabbit_stomp_client.erl @@ -27,13 +27,13 @@ connect0(Version, Login, Pass, Port, Headers) -> %% AMQP default port. {ok, Sock} = gen_tcp:connect(localhost, Port, [{active, false}, binary]), Client0 = recv_state(Sock), - send(Client0, "CONNECT", [{"login", Login}, + send(Client0, 'CONNECT', [{"login", Login}, {"passcode", Pass} | Version] ++ Headers), - {#stomp_frame{command = "CONNECTED"}, Client1} = recv(Client0), + {#stomp_frame{command = 'CONNECTED'}, Client1} = recv(Client0), {ok, Client1}. disconnect(Client = {Sock, _}) -> - send(Client, "DISCONNECT"), + send(Client, 'DISCONNECT'), gen_tcp:close(Sock). send(Client, Command) -> diff --git a/deps/rabbitmq_stomp/test/system_SUITE.erl b/deps/rabbitmq_stomp/test/system_SUITE.erl index 1c724cc39fb4..e85dd09fa915 100644 --- a/deps/rabbitmq_stomp/test/system_SUITE.erl +++ b/deps/rabbitmq_stomp/test/system_SUITE.erl @@ -121,7 +121,7 @@ end_per_testcase0(_, Config) -> publish_no_dest_error(Config) -> Client = ?config(stomp_client, Config), rabbit_stomp_client:send( - Client, "SEND", [{"destination", "/exchange/non-existent"}], ["hello"]), + Client, 'SEND', [{"destination", "/exchange/non-existent"}], ["hello"]), {ok, _Client1, Hdrs, _} = stomp_receive(Client, "ERROR"), "not_found" = proplists:get_value("message", Hdrs), ok. @@ -130,7 +130,7 @@ publish_unauthorized_error(Config) -> ClientFoo = ?config(client_foo, Config), rabbit_stomp_client:send( ClientFoo, "SEND", [{"destination", "/amq/queue/RestrictedQueue"}], ["hello"]), - {ok, _Client1, Hdrs, _} = stomp_receive(ClientFoo, "ERROR"), + {ok, _Client1, Hdrs, _} = stomp_receive(ClientFoo, 'ERROR'), "access_refused" = proplists:get_value("message", Hdrs), ok. @@ -138,8 +138,8 @@ subscribe_error(Config) -> Client = ?config(stomp_client, Config), %% SUBSCRIBE to missing queue rabbit_stomp_client:send( - Client, "SUBSCRIBE", [{"destination", ?DESTINATION}]), - {ok, _Client1, Hdrs, _} = stomp_receive(Client, "ERROR"), + Client, 'SUBSCRIBE', [{"destination", ?DESTINATION}]), + {ok, _Client1, Hdrs, _} = stomp_receive(Client, 'ERROR'), "not_found" = proplists:get_value("message", Hdrs), ok. @@ -152,8 +152,8 @@ subscribe(Config) -> %% subscribe and wait for receipt rabbit_stomp_client:send( - Client, "SUBSCRIBE", [{"destination", ?DESTINATION}, {"receipt", "foo"}]), - {ok, Client1, _, _} = stomp_receive(Client, "RECEIPT"), + Client, 'SUBSCRIBE', [{"destination", ?DESTINATION}, {"receipt", "foo"}]), + {ok, Client1, _, _} = stomp_receive(Client, 'RECEIPT'), %% send from amqp Method = #'basic.publish'{exchange = <<"">>, routing_key = ?QUEUE}, @@ -161,7 +161,7 @@ subscribe(Config) -> amqp_channel:call(Channel, Method, #amqp_msg{props = #'P_basic'{}, payload = <<"hello">>}), - {ok, _Client2, _, [<<"hello">>]} = stomp_receive(Client1, "MESSAGE"), + {ok, _Client2, _, [<<"hello">>]} = stomp_receive(Client1, 'MESSAGE'), ok. subscribe_with_x_priority(Config) -> @@ -211,11 +211,11 @@ unsubscribe_ack(Config) -> auto_delete = true}), %% subscribe and wait for receipt rabbit_stomp_client:send( - Client, "SUBSCRIBE", [{"destination", ?DESTINATION}, + Client, 'SUBSCRIBE', [{"destination", ?DESTINATION}, {"receipt", "rcpt1"}, {"ack", "client"}, {"id", "subscription-id"}]), - {ok, Client1, _, _} = stomp_receive(Client, "RECEIPT"), + {ok, Client1, _, _} = stomp_receive(Client, 'RECEIPT'), %% send from amqp Method = #'basic.publish'{exchange = <<"">>, routing_key = ?QUEUE}, @@ -223,19 +223,19 @@ unsubscribe_ack(Config) -> amqp_channel:call(Channel, Method, #amqp_msg{props = #'P_basic'{}, payload = <<"hello">>}), - {ok, Client2, Hdrs1, [<<"hello">>]} = stomp_receive(Client1, "MESSAGE"), + {ok, Client2, Hdrs1, [<<"hello">>]} = stomp_receive(Client1, 'MESSAGE'), rabbit_stomp_client:send( - Client2, "UNSUBSCRIBE", [{"destination", ?DESTINATION}, - {"id", "subscription-id"}]), + Client2, 'UNSUBSCRIBE', [{"destination", ?DESTINATION}, + {"id", "subscription-id"}]), rabbit_stomp_client:send( - Client2, "ACK", [{rabbit_stomp_util:ack_header_name(Version), + Client2, 'ACK', [{rabbit_stomp_util:ack_header_name(Version), proplists:get_value( rabbit_stomp_util:msg_header_name(Version), Hdrs1)}, {"receipt", "rcpt2"}]), - {ok, _Client3, Hdrs2, _Body2} = stomp_receive(Client2, "ERROR"), + {ok, _Client3, Hdrs2, _Body2} = stomp_receive(Client2, 'ERROR'), ?assertEqual("Subscription not found", proplists:get_value("message", Hdrs2)), ok. @@ -250,10 +250,10 @@ subscribe_ack(Config) -> %% subscribe and wait for receipt rabbit_stomp_client:send( - Client, "SUBSCRIBE", [{"destination", ?DESTINATION}, + Client, 'SUBSCRIBE', [{"destination", ?DESTINATION}, {"receipt", "foo"}, {"ack", "client"}]), - {ok, Client1, _, _} = stomp_receive(Client, "RECEIPT"), + {ok, Client1, _, _} = stomp_receive(Client, 'RECEIPT'), %% send from amqp Method = #'basic.publish'{exchange = <<"">>, routing_key = ?QUEUE}, @@ -261,14 +261,14 @@ subscribe_ack(Config) -> amqp_channel:call(Channel, Method, #amqp_msg{props = #'P_basic'{}, payload = <<"hello">>}), - {ok, _Client2, Headers, [<<"hello">>]} = stomp_receive(Client1, "MESSAGE"), + {ok, _Client2, Headers, [<<"hello">>]} = stomp_receive(Client1, 'MESSAGE'), false = (Version == "1.2") xor proplists:is_defined(?HEADER_ACK, Headers), MsgHeader = rabbit_stomp_util:msg_header_name(Version), AckValue = proplists:get_value(MsgHeader, Headers), AckHeader = rabbit_stomp_util:ack_header_name(Version), - rabbit_stomp_client:send(Client, "ACK", [{AckHeader, AckValue}]), + rabbit_stomp_client:send(Client, 'ACK', [{AckHeader, AckValue}]), #'basic.get_empty'{} = amqp_channel:call(Channel, #'basic.get'{queue = ?QUEUE}), ok. @@ -282,14 +282,14 @@ send(Config) -> %% subscribe and wait for receipt rabbit_stomp_client:send( - Client, "SUBSCRIBE", [{"destination", ?DESTINATION}, {"receipt", "foo"}]), - {ok, Client1, _, _} = stomp_receive(Client, "RECEIPT"), + Client, 'SUBSCRIBE', [{"destination", ?DESTINATION}, {"receipt", "foo"}]), + {ok, Client1, _, _} = stomp_receive(Client, 'RECEIPT'), %% send from stomp rabbit_stomp_client:send( - Client1, "SEND", [{"destination", ?DESTINATION}], ["hello"]), + Client1, 'SEND', [{"destination", ?DESTINATION}], ["hello"]), - {ok, _Client2, _, [<<"hello">>]} = stomp_receive(Client1, "MESSAGE"), + {ok, _Client2, _, [<<"hello">>]} = stomp_receive(Client1, 'MESSAGE'), ok. delete_queue_subscribe(Config) -> @@ -301,14 +301,14 @@ delete_queue_subscribe(Config) -> %% subscribe and wait for receipt rabbit_stomp_client:send( - Client, "SUBSCRIBE", [{"destination", ?DESTINATION}, {"receipt", "bah"}]), - {ok, Client1, _, _} = stomp_receive(Client, "RECEIPT"), + Client, 'SUBSCRIBE', [{"destination", ?DESTINATION}, {"receipt", "bah"}]), + {ok, Client1, _, _} = stomp_receive(Client, 'RECEIPT'), %% delete queue while subscribed #'queue.delete_ok'{} = amqp_channel:call(Channel, #'queue.delete'{queue = ?QUEUE}), - {ok, _Client2, Headers, _} = stomp_receive(Client1, "ERROR"), + {ok, _Client2, Headers, _} = stomp_receive(Client1, 'ERROR'), ?DESTINATION = proplists:get_value("subscription", Headers), @@ -321,7 +321,7 @@ temp_destination_queue(Config) -> #'queue.declare_ok'{} = amqp_channel:call(Channel, #'queue.declare'{queue = ?QUEUE, auto_delete = true}), - rabbit_stomp_client:send( Client, "SEND", [{"destination", ?DESTINATION}, + rabbit_stomp_client:send( Client, 'SEND', [{"destination", ?DESTINATION}, {"reply-to", "/temp-queue/foo"}], ["ping"]), amqp_channel:call(Channel,#'basic.consume'{queue = ?QUEUE, no_ack = true}), @@ -333,22 +333,22 @@ temp_destination_queue(Config) -> ok = amqp_channel:call(Channel, #'basic.publish'{routing_key = ReplyTo}, #amqp_msg{payload = <<"pong">>}), - {ok, _Client1, _, [<<"pong">>]} = stomp_receive(Client, "MESSAGE"), + {ok, _Client1, _, [<<"pong">>]} = stomp_receive(Client, 'MESSAGE'), ok. temp_destination_in_send(Config) -> Client = ?config(stomp_client, Config), - rabbit_stomp_client:send( Client, "SEND", [{"destination", "/temp-queue/foo"}], + rabbit_stomp_client:send( Client, 'SEND', [{"destination", "/temp-queue/foo"}], ["poing"]), - {ok, _Client1, Hdrs, _} = stomp_receive(Client, "ERROR"), + {ok, _Client1, Hdrs, _} = stomp_receive(Client, 'ERROR'), "Invalid destination" = proplists:get_value("message", Hdrs), ok. blank_destination_in_send(Config) -> Client = ?config(stomp_client, Config), - rabbit_stomp_client:send( Client, "SEND", [{"destination", ""}], + rabbit_stomp_client:send( Client, 'SEND', [{"destination", ""}], ["poing"]), - {ok, _Client1, Hdrs, _} = stomp_receive(Client, "ERROR"), + {ok, _Client1, Hdrs, _} = stomp_receive(Client, 'ERROR'), "Invalid destination" = proplists:get_value("message", Hdrs), ok. @@ -359,7 +359,7 @@ stream_filtering(Config) -> %% subscription just to create the stream from STOMP SubDestination = "/topic/stream-queue-test", rabbit_stomp_client:send( - Client, "SUBSCRIBE", + Client, 'SUBSCRIBE', [{"destination", SubDestination}, {"receipt", "foo"}, {"x-queue-name", Stream}, @@ -370,12 +370,12 @@ stream_filtering(Config) -> {"id", "1234"}, {"prefetch-count", "1"}, {"ack", "client"}]), - {ok, Client1, _, _} = stomp_receive(Client, "RECEIPT"), + {ok, Client1, _, _} = stomp_receive(Client, 'RECEIPT'), rabbit_stomp_client:send( - Client1, "UNSUBSCRIBE", [{"destination", SubDestination}, + Client1, 'UNSUBSCRIBE', [{"destination", SubDestination}, {"id", "1234"}, {"receipt", "bar"}]), - {ok, Client2, _, _} = stomp_receive(Client1, "RECEIPT"), + {ok, Client2, _, _} = stomp_receive(Client1, 'RECEIPT'), %% we are going to publish several waves of messages with and without filter values. %% we will then create subscriptions with various filter options @@ -396,8 +396,8 @@ stream_filtering(Config) -> [{"x-stream-filter-value", FilterValue}] ++ Headers0 end, rabbit_stomp_client:send( - C0, "SEND", Headers, ["hello"]), - {ok, C1, _, _} = stomp_receive(C0, "RECEIPT"), + C0, 'SEND', Headers, ["hello"]), + {ok, C1, _, _} = stomp_receive(C0, 'RECEIPT'), C1 end, C, lists:seq(1, WaveCount)) end, @@ -410,7 +410,7 @@ stream_filtering(Config) -> %% filtering on "apple" rabbit_stomp_client:send( - Client5, "SUBSCRIBE", + Client5, 'SUBSCRIBE', [{"destination", StreamDestination}, {"id", "0"}, {"ack", "client"}, @@ -428,14 +428,14 @@ stream_filtering(Config) -> %% we should have only the "apple" messages ?assert(length(AppleFilteredMessages) =:= WaveCount), rabbit_stomp_client:send( - Client6, "UNSUBSCRIBE", [{"destination", StreamDestination}, + Client6, 'UNSUBSCRIBE', [{"destination", StreamDestination}, {"id", "0"}, {"receipt", "bar"}]), {ok, Client7, _, _} = stomp_receive(Client6, "RECEIPT"), %% filtering on "apple" and "orange" rabbit_stomp_client:send( - Client7, "SUBSCRIBE", + Client7, 'SUBSCRIBE', [{"destination", StreamDestination}, {"id", "0"}, {"ack", "client"}, @@ -481,7 +481,7 @@ stream_filtering(Config) -> %% we should have only the "apple" messages and messages without a filter value ?assert(length(AppleUnfilteredFilteredMessages) =:= WaveCount * 2), rabbit_stomp_client:send( - Client10, "UNSUBSCRIBE", [{"destination", StreamDestination}, + Client10, 'UNSUBSCRIBE', [{"destination", StreamDestination}, {"id", "0"}, {"receipt", "bar"}]), {ok, _, _, _} = stomp_receive(Client10, "RECEIPT"), @@ -496,12 +496,12 @@ stomp_receive_messages(Client, Version) -> stomp_receive_messages(Client, Acc, Version) -> try rabbit_stomp_client:recv(Client) of - {#stomp_frame{command = "MESSAGE", + {#stomp_frame{command = 'MESSAGE', headers = Headers}, Client1} -> MsgHeader = rabbit_stomp_util:msg_header_name(Version), AckValue = proplists:get_value(MsgHeader, Headers), AckHeader = rabbit_stomp_util:ack_header_name(Version), - rabbit_stomp_client:send(Client1, "ACK", [{AckHeader, AckValue}]), + rabbit_stomp_client:send(Client1, 'ACK', [{AckHeader, AckValue}]), stomp_receive_messages(Client1, [Headers] ++ Acc, Version) catch error:{badmatch, {error, timeout}} -> diff --git a/deps/rabbitmq_web_stomp/src/rabbit_web_stomp_handler.erl b/deps/rabbitmq_web_stomp/src/rabbit_web_stomp_handler.erl index 5f98feaf25a2..8e6637435159 100644 --- a/deps/rabbitmq_web_stomp/src/rabbit_web_stomp_handler.erl +++ b/deps/rabbitmq_web_stomp/src/rabbit_web_stomp_handler.erl @@ -30,6 +30,7 @@ takeover/7]). -record(state, { + conn_name, frame_type, heartbeat_mode, heartbeat, @@ -95,13 +96,20 @@ init(Req0, Opts) -> auth_hd = cowboy_req:header(<<"authorization">>, Req) }, WsOpts}. -websocket_init(State) -> - process_flag(trap_exit, true), - {ok, ProcessorState} = init_processor_state(State), - {ok, rabbit_event:init_stats_timer( - State#state{proc_state = ProcessorState, - parse_state = rabbit_stomp_frame:initial_state()}, - #state.stats_timer)}. +websocket_init(State = #state{socket = Sock}) -> + case rabbit_net:connection_string(Sock, inbound) of + {ok, ConnStr} -> + ?LOG_INFO("Accepting Web STOMP connection ~s", [ConnStr]), + process_flag(trap_exit, true), + {ok, ProcessorState} = init_processor_state(State), + {ok, rabbit_event:init_stats_timer( + State#state{proc_state = ProcessorState, + parse_state = rabbit_stomp_frame:initial_state(), + conn_name = rabbit_data_coercion:to_binary(ConnStr)}, + #state.stats_timer)}; + {error, Reason} -> + {[{shutdown_reason, Reason}], State} + end. -spec close_connection(pid(), string()) -> 'ok'. close_connection(Pid, Reason) -> @@ -165,48 +173,9 @@ websocket_info({bump_credit, Msg}, State) -> credit_flow:handle_bump_msg(Msg), handle_credits(control_throttle(State)); -websocket_info(#'basic.consume_ok'{}, State) -> - {ok, State}; -websocket_info(#'basic.cancel_ok'{}, State) -> - {ok, State}; -websocket_info(#'basic.ack'{delivery_tag = Tag, multiple = IsMulti}, - State=#state{ proc_state = ProcState0 }) -> - ProcState = rabbit_stomp_processor:flush_pending_receipts(Tag, - IsMulti, - ProcState0), - {ok, State#state{ proc_state = ProcState }}; -websocket_info({Delivery = #'basic.deliver'{}, - #amqp_msg{props = Props, payload = Payload}, - DeliveryCtx}, - State=#state{ proc_state = ProcState0 }) -> - ProcState = rabbit_stomp_processor:send_delivery(Delivery, - Props, - Payload, - DeliveryCtx, - ProcState0), - {ok, State#state{ proc_state = ProcState }}; -websocket_info({Delivery = #'basic.deliver'{}, - #amqp_msg{props = Props, payload = Payload}}, - State=#state{ proc_state = ProcState0 }) -> - ProcState = rabbit_stomp_processor:send_delivery(Delivery, - Props, - Payload, - undefined, - ProcState0), - {ok, State#state{ proc_state = ProcState }}; -websocket_info(#'basic.cancel'{consumer_tag = Ctag}, - State=#state{ proc_state = ProcState0 }) -> - case rabbit_stomp_processor:cancel_consumer(Ctag, ProcState0) of - {ok, ProcState, _Connection} -> - {ok, State#state{ proc_state = ProcState }}; - {stop, _Reason, ProcState} -> - stop(State#state{ proc_state = ProcState }) - end; - websocket_info({start_heartbeats, _}, State = #state{heartbeat_mode = no_heartbeat}) -> {ok, State}; - websocket_info({start_heartbeats, {0, 0}}, State) -> {ok, State}; websocket_info({start_heartbeats, {SendTimeout, ReceiveTimeout}}, @@ -219,20 +188,34 @@ websocket_info({start_heartbeats, {SendTimeout, ReceiveTimeout}}, Heartbeat = rabbit_heartbeat:start(SupPid, Sock, SendTimeout, SendFun, ReceiveTimeout, ReceiveFun), {ok, State#state{heartbeat = Heartbeat}}; +websocket_info(connection_created, State) -> + Infos = infos(?INFO_ITEMS ++ ?OTHER_METRICS, State), + ?LOG_DEBUG("Connection created infos ~p", [Infos]), + rabbit_core_metrics:connection_created(self(), Infos), + rabbit_event:notify(connection_created, Infos), + {[], State, hibernate}; websocket_info(client_timeout, State) -> stop(State); +websocket_info({'$gen_cast', QueueEvent = {queue_event, _, _}}, State) -> + ProcState = processor_state(State), + case rabbit_stomp_processor:handle_queue_event(QueueEvent, ProcState) of + {ok, NewProcState} -> + {[{active, true}], processor_state(NewProcState, State), hibernate}; + {error, _Reason, NewProcState} -> + stop(State#state{proc_state = NewProcState}) + end; %%---------------------------------------------------------------------------- -websocket_info({'EXIT', From, Reason}, - State=#state{ proc_state = ProcState0 }) -> - case rabbit_stomp_processor:handle_exit(From, Reason, ProcState0) of - {stop, _Reason, ProcState} -> - stop(State#state{ proc_state = ProcState }); - unknown_exit -> - %% Allow the server to send remaining error messages - self() ! close_websocket, - {ok, State} - end; +%% websocket_info({'EXIT', From, Reason}, +%% State=#state{ proc_state = ProcState0 }) -> +%% case rabbit_stomp_processor:handle_exit(From, Reason, ProcState0) of +%% {stop, _Reason, ProcState} -> +%% stop(State#state{ proc_state = ProcState }); +%% unknown_exit -> +%% %% Allow the server to send remaining error messages +%% self() ! close_websocket, +%% {ok, State} +%% end; websocket_info(close_websocket, State) -> stop(State); @@ -248,7 +231,11 @@ websocket_info(Msg, State) -> terminate(_Reason, _Req, #state{proc_state = undefined}) -> ok; -terminate(_Reason, _Req, #state{proc_state = ProcState}) -> +terminate(_Reason, _Req, State = #state{proc_state = ProcState}) -> + rabbit_core_metrics:connection_closed(self()), + Infos = infos(?OTHER_METRICS, State), + rabbit_event:notify(connection_closed, Infos), + rabbit_networking:unregister_non_amqp_connection(self()), _ = rabbit_stomp_processor:flush_and_die(ProcState), ok. @@ -289,14 +276,13 @@ handle_data1(Bytes, State = #state{proc_state = ProcState, {ok, ensure_stats_timer(State#state{ parse_state = ParseState1 })}; {ok, Frame, Rest} -> case rabbit_stomp_processor:process_frame(Frame, ProcState) of - {ok, ProcState1, ConnPid} -> + {ok, ProcState1} -> ParseState1 = rabbit_stomp_frame:initial_state(), State1 = maybe_block(State, Frame), handle_data1( Rest, State1 #state{ parse_state = ParseState1, - proc_state = ProcState1, - connection = ConnPid }); + proc_state = ProcState1}); {stop, _Reason, ProcState1} -> %% do not exit here immediately, because we need to wait for messages eventually enqueued by process_request self() ! close_websocket, @@ -357,19 +343,56 @@ maybe_emit_stats(State) -> rabbit_event:if_enabled(State, #state.stats_timer, fun() -> emit_stats(State) end). -emit_stats(State=#state{connection = C}) when C == none; C == undefined -> - %% Avoid emitting stats on terminate when the connection has not yet been - %% established, as this causes orphan entries on the stats database - State1 = rabbit_event:reset_stats_timer(State, #state.stats_timer), - State1; -emit_stats(State=#state{socket=Sock, state=RunningState, connection=Conn}) -> +%% emit_stats(State=#state{connection = C}) when C == none; C == undefined -> +%% %% Avoid emitting stats on terminate when the connection has not yet been +%% %% established, as this causes orphan entries on the stats database +%% State1 = rabbit_event:reset_stats_timer(State, #state.stats_timer), +%% State1; +emit_stats(State=#state{socket=Sock, state=RunningState}) -> SockInfos = case rabbit_net:getstat(Sock, [recv_oct, recv_cnt, send_oct, send_cnt, send_pend]) of {ok, SI} -> SI; {error, _} -> [] end, - Infos = [{pid, Conn}, {state, RunningState}|SockInfos], - rabbit_core_metrics:connection_stats(Conn, Infos), + Infos = [{pid, self()}, {state, RunningState}|SockInfos], + rabbit_core_metrics:connection_stats(self(), Infos), rabbit_event:notify(connection_stats, Infos), State1 = rabbit_event:reset_stats_timer(State, #state.stats_timer), State1. + +infos(Items, State) -> [{Item, info_internal(Item, State)} || Item <- Items]. + +info_internal(pid, _) -> self(); +info_internal(SockStat, #state{socket = Sock}) when SockStat =:= recv_oct; + SockStat =:= recv_cnt; + SockStat =:= send_oct; + SockStat =:= send_cnt; + SockStat =:= send_pend -> + case rabbit_net:getstat(Sock, [SockStat]) of + {ok, [{_, N}]} when is_number(N) -> N; + _ -> 0 + end; +info_internal(state, State) -> info_internal(connection_state, State); +info_internal(garbage_collection, _State) -> + rabbit_misc:get_gc_info(self()); +info_internal(reductions, _State) -> + {reductions, Reductions} = erlang:process_info(self(), reductions), + Reductions; +info_internal(timeout, State) -> + %% TODO: real value + 0; +info_internal(conn_name, #state{conn_name = Val}) -> + rabbit_data_coercion:to_binary(Val); +info_internal(name, #state{conn_name = Val}) -> + rabbit_data_coercion:to_binary(Val); +info_internal(connection, #state{connection = _Val}) -> + self(); +info_internal(connection_state, #state{state = Val}) -> + Val; +info_internal(Key, #state{proc_state = ProcState}) -> + rabbit_stomp_processor:info(Key, ProcState). + + +processor_state(#state{proc_state = ProcState }) -> ProcState. +processor_state(ProcState, #state{} = State) -> + State#state{proc_state = ProcState}. diff --git a/deps/rabbitmq_web_stomp/test/cowboy_websocket_SUITE.erl b/deps/rabbitmq_web_stomp/test/cowboy_websocket_SUITE.erl index 625c0b02a8db..9a268423a17a 100644 --- a/deps/rabbitmq_web_stomp/test/cowboy_websocket_SUITE.erl +++ b/deps/rabbitmq_web_stomp/test/cowboy_websocket_SUITE.erl @@ -187,7 +187,7 @@ sub_non_existent(Config) -> ok = raw_send(WS, "SUBSCRIBE", [{"destination", "/exchange/doesnotexist"}, {"id", "s0"}]), - {<<"ERROR">>, [{<<"message">>,<<"not_found">>} | _Tail ], <<"NOT_FOUND - no exchange 'doesnotexist' in vhost '/'">>} = raw_recv(WS), + {<<"ERROR">>, [{<<"message">>,<<"not_found">>} | _Tail ], <<"no exchange 'doesnotexist' in vhost '/'">>} = raw_recv(WS), {close, _} = raw_recv(WS), ok.