diff --git a/big_tests/tests/acc_e2e_SUITE_data/acc_test_helper.erl b/big_tests/tests/acc_e2e_SUITE_data/acc_test_helper.erl index c408e0a0dcc..a4c85f3c8f7 100644 --- a/big_tests/tests/acc_e2e_SUITE_data/acc_test_helper.erl +++ b/big_tests/tests/acc_e2e_SUITE_data/acc_test_helper.erl @@ -52,7 +52,7 @@ drop_if_jid_not_mine(X) -> recreate_table() -> try ets:delete(test_message_index) catch _:_ -> ok end, - ets:new(test_message_index, [named_table, public, {heir, whereis(ejabberd_c2s_sup), none}]). + ets:new(test_message_index, [named_table, public, {heir, whereis(mongoose_c2s_sup), none}]). check_acc(#{ stanza := #{ type := <<"chat">> } } = Acc) -> Ref = mongoose_acc:ref(Acc), diff --git a/big_tests/tests/component_SUITE.erl b/big_tests/tests/component_SUITE.erl index 476e429c90b..58006908395 100644 --- a/big_tests/tests/component_SUITE.erl +++ b/big_tests/tests/component_SUITE.erl @@ -54,15 +54,14 @@ all() -> ]. groups() -> - G = [{xep0114_tcp, [], xep0114_tests()}, - {xep0114_ws, [], xep0114_tests()}, - {subdomain, [], [register_subdomain]}, - {hidden_components, [], [disco_with_hidden_component]}, - {distributed, [], [register_in_cluster, - register_same_on_both - %clear_on_node_down TODO: Breaks cover - ]}], - ct_helper:repeat_all_until_all_ok(G). + [{xep0114_tcp, [], xep0114_tests()}, + {xep0114_ws, [], xep0114_tests()}, + {subdomain, [], [register_subdomain]}, + {hidden_components, [], [disco_with_hidden_component]}, + {distributed, [], [register_in_cluster, + register_same_on_both + %clear_on_node_down TODO: Breaks cover + ]}]. suite() -> require_rpc_nodes([mim]) ++ escalus:suite(). diff --git a/big_tests/tests/mongoose_helper.erl b/big_tests/tests/mongoose_helper.erl index 45b91a21286..16706685486 100644 --- a/big_tests/tests/mongoose_helper.erl +++ b/big_tests/tests/mongoose_helper.erl @@ -237,7 +237,7 @@ wait_for_session_count(Expected) -> wait_until(fun() -> length(get_session_specs()) end, Expected, #{name => session_count}). get_session_specs() -> - rpc(mim(), supervisor, which_children, [ejabberd_c2s_sup]). + rpc(mim(), supervisor, which_children, [mongoose_c2s_sup]). get_session_pids() -> [element(2, X) || X <- get_session_specs()]. diff --git a/big_tests/tests/sm_helper.erl b/big_tests/tests/sm_helper.erl index dcf15ada9a0..694f7d7c5d7 100644 --- a/big_tests/tests/sm_helper.erl +++ b/big_tests/tests/sm_helper.erl @@ -235,7 +235,7 @@ wait_for_c2s_unacked_count(C2SPid, Count) -> get_c2s_unacked_count(C2SPid) -> StateData = mongoose_helper:get_c2s_state_data(C2SPid), - SmStateData = rpc(mim(), mongoose_c2s, get_mod_state, [StateData, mod_stream_management]), + {ok, SmStateData} = rpc(mim(), mongoose_c2s, get_mod_state, [StateData, mod_stream_management]), element(3, SmStateData). wait_for_resource_count(Client, N) -> diff --git a/big_tests/tests/websockets_SUITE.erl b/big_tests/tests/websockets_SUITE.erl index c6c6d39872e..5cef2172793 100644 --- a/big_tests/tests/websockets_SUITE.erl +++ b/big_tests/tests/websockets_SUITE.erl @@ -31,10 +31,8 @@ all() -> {group, wss_chat}]. groups() -> - G = [{ws_chat, [sequence], test_cases()}, - {wss_chat, [sequence], test_cases()} - ], - ct_helper:repeat_all_until_all_ok(G). + [{ws_chat, [sequence], test_cases()}, + {wss_chat, [sequence], test_cases()}]. test_cases() -> [chat_msg, @@ -111,4 +109,3 @@ escape_attrs(Config) -> special_chars_helper:check_attr_from_to(Geralt, Carol) end). - diff --git a/doc/developers-guide/accumulators.md b/doc/developers-guide/accumulators.md index 458a6ae39b0..145c671ed2d 100644 --- a/doc/developers-guide/accumulators.md +++ b/doc/developers-guide/accumulators.md @@ -56,7 +56,7 @@ While allowed, stanza-less accumulators usage should be avoided. * `element(t())` * `to_jid(t())` * `from_jid(t())` -* `packet(t())` - Returns an `ejabberd_c2s:packet()` if there is a stanza in the accumulator. +* `packet(t())` - Returns an `mongoose_c2s:packet()` if there is a stanza in the accumulator. * `stanza_name(t())` - Returns `name` value from `stanza` map. * `stanza_type(t())` - Returns `type` value from `stanza` map. * `stanza_ref(t())` - Returns `ref` value from `stanza` map. This is not the same as `ref(t())`! diff --git a/include/ejabberd_c2s.hrl b/include/ejabberd_c2s.hrl deleted file mode 100644 index 76d22f5ec5e..00000000000 --- a/include/ejabberd_c2s.hrl +++ /dev/null @@ -1,141 +0,0 @@ --include("mod_privacy.hrl"). - --define(STREAM_MGMT_H_MAX, (1 bsl 32 - 1)). --define(CONSTRAINT_CHECK_TIMEOUT, 5). %% seconds - --type jid_set() :: gb_sets:set(jid:simple_jid()). - --type authenticated_state() :: boolean() | resumed | replaced. - --type debug_presences() :: {atom(), non_neg_integer()}. - -%% pres_a contains all the presence available send (either through roster mechanism or directed). -%% Directed presence unavailable remove user from pres_a. --record(state, {socket, - sockmod :: ejabberd:sockmod(), - socket_monitor, - xml_socket, - streamid :: undefined | binary(), - sasl_state, - access, - shaper, - zlib = {false, 0} :: {boolean(), integer()}, - tls_enabled = false :: boolean(), - tls_options :: mongoose_tls:options() | undefined, - tls_mode :: no_tls | tls | starttls | starttls_required, - authenticated = false :: authenticated_state(), - host_type :: binary() | undefined, - jid :: jid:jid() | undefined, - user = <<>> :: jid:user(), - server = <<>> :: jid:server(), - resource = <<>> :: jid:resource(), - sid :: ejabberd_sm:sid() | undefined, - %% We have _subscription to_ these users' presence status; - %% i.e. they send us presence updates. - %% This comes from the roster. - pres_t = gb_sets:new() :: jid_set() | debug_presences(), - %% We have _subscription from_ these users, - %% i.e. they have subscription to us. - %% We send them presence updates. - %% This comes from the roster. - pres_f = gb_sets:new() :: jid_set() | debug_presences(), - %% We're _available_ to these users, - %% i.e. we broadcast presence updates to them. - %% This may change throughout the session. - pres_a = gb_sets:new() :: jid_set() | debug_presences(), - %% We are _invisible_ to these users. - %% This may change throughout the session. - pres_i = gb_sets:new() :: jid_set() | debug_presences(), - pending_invitations = [], - pres_last, pres_pri, - pres_timestamp :: integer() | undefined, % unix time in seconds - %% Are we invisible? - pres_invis = false :: boolean(), - privacy_list = #userlist{} :: mongoose_privacy:userlist(), - conn = unknown, - auth_module :: ejabberd_auth:authmodule(), - ip :: {inet:ip_address(), inet:port_number()} | undefined, - aux_fields = [] :: [{aux_key(), aux_value()}], - lang :: ejabberd:lang(), - stream_mgmt = false, - stream_mgmt_in = 0, - stream_mgmt_id, - stream_mgmt_out_acked = 0, - stream_mgmt_buffer = [] :: [mongoose_acc:t()], - stream_mgmt_buffer_size = 0, - stream_mgmt_buffer_max, - stream_mgmt_ack_freq, - stream_mgmt_resume_timeout, - stream_mgmt_resume_tref, - stream_mgmt_resumed_from, - stream_mgmt_constraint_check_tref, - csi_state = active :: mod_csi:state(), - csi_buffer = [] :: [mongoose_acc:t()], - hibernate_after = 0 :: non_neg_integer(), - replaced_pids = [] :: [{MonitorRef :: reference(), ReplacedPid :: pid()}], - handlers = #{} :: #{ term() => {module(), atom(), term()} }, - cred_opts :: mongoose_credentials:opts() - }). --type aux_key() :: atom(). --type aux_value() :: any(). --type state() :: #state{}. - --type statename() :: atom(). --type conntype() :: 'c2s' - | 'c2s_compressed' - | 'c2s_compressed_tls' - | 'c2s_tls' - | 'http_bind' - | 'http_poll' - | 'unknown'. - -%% FSM handler return value --type fsm_return() :: {'stop', Reason :: 'normal', state()} - | {'next_state', statename(), state()} - | {'next_state', statename(), state(), Timeout :: integer()}. - --type blocking_type() :: 'block' | 'unblock'. - --type broadcast_type() :: {exit, Reason :: binary()} - | {item, IJID :: jid:simple_jid() | jid:jid(), - ISubscription :: from | to | both | none | remove} - | {privacy_list, PrivList :: mongoose_privacy:userlist(), - PrivListName :: binary()} - | {blocking, UserList :: mongoose_privacy:userlist(), What :: blocking_type(), - [binary()]} - | unknown. - --type broadcast() :: {broadcast, broadcast_type() | mongoose_acc:t()}. - --type broadcast_result() :: {new_state, NewState :: state()} - | {exit, Reason :: binary()} - | {send_new, From :: jid:jid(), To :: jid:jid(), - Packet :: exml:element(), - NewState :: state()}. - --type routing_result_atom() :: allow | deny | forbidden | ignore | block | invalid | probe. - --type routing_result() :: {DoRoute :: routing_result_atom(), NewAcc :: mongoose_acc:t(), - NewState :: state()} - | {DoRoute :: routing_result_atom(), NewAcc :: mongoose_acc:t(), - NewPacket :: exml:element(), NewState :: state()}. - -%-define(DBGFSM, true). --ifdef(DBGFSM). --define(FSMOPTS, [{debug, [trace]}]). --else. --define(FSMOPTS, []). --endif. - -%% Module start with or without supervisor: --ifdef(NO_TRANSIENT_SUPERVISORS). --define(SUPERVISOR_START(SockData, Opts), - ?GEN_FSM:start(ejabberd_c2s, [SockData, Opts], ?FSMOPTS ++ fsm_limit_opts(Opts))). --else. --define(SUPERVISOR_START(SockData, Opts), - supervisor:start_child(ejabberd_c2s_sup, [SockData, Opts])). --endif. - -%% This is the timeout to apply between event when starting a new -%% session: --define(C2S_OPEN_TIMEOUT, 60000). diff --git a/include/jlib.hrl b/include/jlib.hrl index d5a20c55496..65a64bcd13f 100644 --- a/include/jlib.hrl +++ b/include/jlib.hrl @@ -35,5 +35,6 @@ -record(xmlstreamerror, {name :: binary()}). -define(STREAM_TRAILER, <<"">>). +-define(XML_STREAM_TRAILER, #xmlstreamend{name = <<"stream:stream">>}). -endif. diff --git a/rebar.config b/rebar.config index 4416a2a6ca7..94a200dcef9 100644 --- a/rebar.config +++ b/rebar.config @@ -112,8 +112,7 @@ %%% Other {pa, {git, "https://github.com/erszcz/pa.git", {branch, "master"}}}, - %% MR of jwerl - https://gitlab.com/glejeune/jwerl/-/merge_requests/13 - {jwerl, {git, "https://gitlab.com/vkatsuba/jwerl.git", {branch, "refactoring/otp-24"}}}, + {jwerl, "1.2.0"}, {cpool, "0.1.0"}, %% Do not upgrade cpool to version 0.1.1, it has bugs {nkpacket, {git, "https://github.com/michalwski/nkpacket.git", {ref, "f7c5349"}}}, {nksip, {git, "https://github.com/NetComposer/nksip.git", {ref, "1a29ef3"}}} diff --git a/rebar.lock b/rebar.lock index e07ff4826c2..4c3dcfd73bd 100644 --- a/rebar.lock +++ b/rebar.lock @@ -61,10 +61,7 @@ {<<"jid">>,{pkg,<<"mongoose_jid">>,<<"2.0.0">>},0}, {<<"jiffy">>,{pkg,<<"jiffy">>,<<"1.1.1">>},0}, {<<"jsx">>,{pkg,<<"jsx">>,<<"2.9.0">>},1}, - {<<"jwerl">>, - {git,"https://gitlab.com/vkatsuba/jwerl.git", - {ref,"d03607fd14a6a7556f01014af84903a3df60ff5d"}}, - 0}, + {<<"jwerl">>,{pkg,<<"jwerl">>,<<"1.2.0">>},0}, {<<"lager">>,{pkg,<<"lager">>,<<"3.9.2">>},0}, {<<"lasse">>,{pkg,<<"lasse">>,<<"1.2.0">>},0}, {<<"lhttpc">>,{pkg,<<"lhttpc">>,<<"1.6.2">>},1}, @@ -172,6 +169,7 @@ {<<"jid">>, <<"0D0FD1130EAD05DF672D1C9E0405D3000F0C87A7D446577755CF5EB173104FE9">>}, {<<"jiffy">>, <<"ACA10F47AA91697BF24AB9582C74E00E8E95474C7EF9F76D4F1A338D0F5DE21B">>}, {<<"jsx">>, <<"D2F6E5F069C00266CAD52FB15D87C428579EA4D7D73A33669E12679E203329DD">>}, + {<<"jwerl">>, <<"2A0EC870BE0B803BBEEE1463DC5AE05E3D64EEAE2ABC7DB4250ABC611FE7E320">>}, {<<"lager">>, <<"4CAB289120EB24964E3886BD22323CB5FEFE4510C076992A23AD18CF85413D8C">>}, {<<"lasse">>, <<"DB8F06983B235F6C4C86AC79BCF23EDDCA16FCCCAD1EB45F222852BB133BD793">>}, {<<"lhttpc">>, <<"044F16F0018C7AA7E945E9E9406C7F6035E0B8BC08BF77B00C78CE260E1071E3">>}, @@ -234,6 +232,7 @@ {<<"jid">>, <<"7BAE8403552D5BCECD1CD2A848445B5FC3533F5EEB355173A0531A1A1AA8E008">>}, {<<"jiffy">>, <<"62E1F0581C3C19C33A725C781DFA88410D8BFF1BBAFC3885A2552286B4785C4C">>}, {<<"jsx">>, <<"8EE1DB1CABAFDD578A2776A6AAAE87C2A8CE54B47B59E9EC7DAB5D7EB71CD8DC">>}, + {<<"jwerl">>, <<"894C275CA4CF3A4F293541927CA3D7A2622B21D8BD70580EF70EACB0425F1941">>}, {<<"lager">>, <<"7F904D9E87A8CB7E66156ED31768D1C8E26EBA1D54F4BC85B1AA4AC1F6340C28">>}, {<<"lasse">>, <<"8081769683F73D76A718319A593F8551A0D3747404C51CCE5869CFC6AEDC7EDF">>}, {<<"lhttpc">>, <<"76B5FA6149D1E10D4B1FBC4EBD51D371DB19C1AB9F0A9ECF5B526440DF064E97">>}, diff --git a/src/c2s/mongoose_c2s.erl b/src/c2s/mongoose_c2s.erl index 13135666eef..55b23d7966e 100644 --- a/src/c2s/mongoose_c2s.erl +++ b/src/c2s/mongoose_c2s.erl @@ -12,7 +12,7 @@ -export([callback_mode/0, init/1, handle_event/4, terminate/3]). %% utils --export([start_link/2, stop/2, exit/2]). +-export([start_link/2, start/2, stop/2, exit/2, async/3]). -export([get_host_type/1, get_lserver/1, get_sid/1, get_jid/1, get_mod_state/2, merge_mod_state/2, remove_mod_state/2, get_ip/1, get_socket/1, get_lang/1, get_stream_id/1]). @@ -31,11 +31,11 @@ socket :: undefined | mongoose_c2s_socket:socket(), parser :: undefined | exml_stream:parser(), shaper :: undefined | shaper:shaper(), - listener_opts :: mongoose_listener:options(), + listener_opts :: listener_opts(), auth_module :: undefined | module(), state_mod = #{} :: #{module() => term()} }). --type c2s_data() :: #c2s_data{} | ejabberd_c2s:state(). +-type c2s_data() :: #c2s_data{}. -type maybe_ok() :: ok | {error, atom()}. -type fsm_res() :: gen_statem:event_handler_result(c2s_state(), c2s_data()). -type packet() :: {jid:jid(), jid:jid(), exml:element()}. @@ -50,7 +50,13 @@ | wait_for_session_establishment | session_established. --export_type([packet/0, c2s_data/0, c2s_state/0]). +-type listener_opts() :: #{shaper := atom(), + max_stanza_size := non_neg_integer(), + backwards_compatible_session := boolean(), + c2s_state_timeout := non_neg_integer(), + term() => term()}. + +-export_type([packet/0, c2s_data/0, c2s_state/0, listener_opts/0]). %%%---------------------------------------------------------------------- %%% gen_statem @@ -60,20 +66,20 @@ callback_mode() -> handle_event_function. --spec init({ranch:ref(), ranch_tcp, mongoose_listener:options()}) -> +-spec init({module(), term(), listener_opts()}) -> gen_statem:init_result(c2s_state(), c2s_data()). -init({RanchRef, ranch_tcp, LOpts}) -> +init({SocketModule, SocketOpts, LOpts}) -> StateData = #c2s_data{listener_opts = LOpts}, - ConnectEvent = {next_event, internal, {connect, RanchRef}}, + ConnectEvent = {next_event, internal, {connect, {SocketModule, SocketOpts}}}, {ok, connect, StateData, ConnectEvent}. -spec handle_event(gen_statem:event_type(), term(), c2s_state(), c2s_data()) -> fsm_res(). -handle_event(internal, {connect, RanchRef}, connect, +handle_event(internal, {connect, {SocketModule, SocketOpts}}, connect, StateData = #c2s_data{listener_opts = #{shaper := ShaperName, max_stanza_size := MaxStanzaSize} = LOpts}) -> {ok, Parser} = exml_stream:new_parser([{max_child_size, MaxStanzaSize}]), Shaper = shaper:new(ShaperName), - C2SSocket = mongoose_c2s_socket:new_socket(RanchRef, LOpts), + C2SSocket = mongoose_c2s_socket:new(SocketModule, SocketOpts, LOpts), StateData1 = StateData#c2s_data{socket = C2SSocket, parser = Parser, shaper = Shaper}, {next_state, {wait_for_stream, stream_start}, StateData1, state_timeout(LOpts)}; @@ -185,15 +191,18 @@ terminate(Reason, C2SState, #c2s_data{host_type = HostType, lserver = LServer} = -spec handle_socket_data(c2s_data(), {_, _, iodata()}) -> fsm_res(). handle_socket_data(StateData = #c2s_data{socket = Socket}, Payload) -> - case mongoose_c2s_socket:handle_socket_data(Socket, Payload) of + case mongoose_c2s_socket:handle_data(Socket, Payload) of {error, _Reason} -> {stop, {shutdown, socket_error}, StateData}; Data -> handle_socket_packet(StateData, Data) end. --spec handle_socket_packet(c2s_data(), iodata()) -> fsm_res(). -handle_socket_packet(StateData = #c2s_data{parser = Parser, shaper = Shaper}, Packet) -> +-spec handle_socket_packet(c2s_data(), iodata() | {raw, [exml_stream:element()]}) -> fsm_res(). +handle_socket_packet(StateData, {raw, Elements}) -> + ?LOG_DEBUG(#{what => received_raw_on_stream, elements => Elements, c2s_pid => self()}), + handle_socket_elements(StateData, Elements, 0); +handle_socket_packet(StateData = #c2s_data{parser = Parser}, Packet) -> ?LOG_DEBUG(#{what => received_xml_on_stream, packet => Packet, c2s_pid => self()}), case exml_stream:parse(Parser, Packet) of {error, Reason} -> @@ -201,19 +210,24 @@ handle_socket_packet(StateData = #c2s_data{parser = Parser, shaper = Shaper}, Pa {keep_state, StateData, NextEvent}; {ok, NewParser, XmlElements} -> Size = iolist_size(Packet), - {NewShaper, Pause} = shaper:update(Shaper, Size), - mongoose_metrics:update(global, [data, xmpp, received, xml_stanza_size], Size), - NewStateData = StateData#c2s_data{parser = NewParser, shaper = NewShaper}, - MaybePauseTimeout = maybe_pause(NewStateData, Pause), - StreamEvents = [ {next_event, internal, XmlEl} || XmlEl <- XmlElements ], - {keep_state, NewStateData, MaybePauseTimeout ++ StreamEvents} + NewStateData = StateData#c2s_data{parser = NewParser}, + handle_socket_elements(NewStateData, XmlElements, Size) end. +-spec handle_socket_elements(c2s_data(), [exml:element()], non_neg_integer()) -> fsm_res(). +handle_socket_elements(StateData = #c2s_data{shaper = Shaper}, Elements, Size) -> + {NewShaper, Pause} = shaper:update(Shaper, Size), + mongoose_metrics:update(global, [data, xmpp, received, xml_stanza_size], Size), + NewStateData = StateData#c2s_data{shaper = NewShaper}, + MaybePauseTimeout = maybe_pause(NewStateData, Pause), + StreamEvents = [ {next_event, internal, XmlEl} || XmlEl <- Elements ], + {keep_state, NewStateData, MaybePauseTimeout ++ StreamEvents}. + -spec maybe_pause(c2s_data(), integer()) -> any(). maybe_pause(_StateData, Pause) when Pause > 0 -> [{{timeout, activate_socket}, Pause, activate_socket}]; maybe_pause(#c2s_data{socket = Socket}, _) -> - mongoose_c2s_socket:activate_socket(Socket), + mongoose_c2s_socket:activate(Socket), []. -spec close_socket(c2s_data()) -> ok | {error, term()}. @@ -222,12 +236,7 @@ close_socket(#c2s_data{socket = Socket}) -> -spec activate_socket(c2s_data()) -> ok | {error, term()}. activate_socket(#c2s_data{socket = Socket}) -> - mongoose_c2s_socket:activate_socket(Socket). - --spec send_text(c2s_data(), iodata()) -> ok | {error, term()}. -send_text(#c2s_data{socket = Socket}, Text) -> - mongoose_metrics:update(global, [data, xmpp, sent, xml_stanza_size], iolist_size(Text)), - mongoose_c2s_socket:send_text(Socket, Text). + mongoose_c2s_socket:activate(Socket). -spec filter_mechanism(c2s_data(), binary()) -> boolean(). filter_mechanism(#c2s_data{socket = Socket}, <<"SCRAM-SHA-1-PLUS">>) -> @@ -537,13 +546,21 @@ maybe_retry_state(StateData = #c2s_data{listener_opts = LOpts}, C2SState) -> end. -spec handle_cast(c2s_data(), c2s_state(), term()) -> fsm_res(). -handle_cast(StateData, _C2SState, {exit, Reason}) -> +handle_cast(StateData, _C2SState, {exit, Reason}) when is_binary(Reason) -> StreamConflict = mongoose_xmpp_errors:stream_conflict(StateData#c2s_data.lang, Reason), send_element_from_server_jid(StateData, StreamConflict), send_trailer(StateData), {stop, {shutdown, Reason}}; +handle_cast(StateData, _C2SState, {exit, system_shutdown}) -> + Error = mongoose_xmpp_errors:system_shutdown(), + send_element_from_server_jid(StateData, Error), + send_trailer(StateData), + {stop, {shutdown, system_shutdown}}; handle_cast(StateData, C2SState, {stop, Reason}) -> handle_stop_request(StateData, C2SState, Reason); +handle_cast(_StateData, _C2SState, {async, Fun, Args}) -> + apply(Fun, Args), + keep_state_and_data; handle_cast(StateData, C2SState, Event) -> handle_foreign_event(StateData, C2SState, cast, Event). @@ -552,8 +569,6 @@ handle_info(StateData, C2SState, #xmlel{} = El) -> handle_c2s_packet(StateData, C2SState, El); handle_info(StateData, C2SState, {route, Acc}) -> handle_stanza_to_client(StateData, C2SState, Acc); -handle_info(StateData, C2SState, {route, _From, _To, Acc}) -> - handle_stanza_to_client(StateData, C2SState, Acc); handle_info(StateData, _C2SState, {TcpOrSSl, _Socket, _Packet} = SocketData) when TcpOrSSl =:= tcp; TcpOrSSl =:= ssl -> handle_socket_data(StateData, SocketData); @@ -563,11 +578,6 @@ handle_info(StateData, C2SState, {Closed, _Socket} = SocketData) handle_info(StateData, C2SState, {Error, _Socket} = SocketData) when Error =:= tcp_error; Error =:= ssl_error -> handle_socket_error(StateData, C2SState, SocketData); -handle_info(StateData, _C2SState, replaced) -> - StreamConflict = mongoose_xmpp_errors:stream_conflict(), - send_element_from_server_jid(StateData, StreamConflict), - send_trailer(StateData), - {stop, {shutdown, replaced}}; handle_info(StateData, C2SState, Info) -> handle_foreign_event(StateData, C2SState, info, Info). @@ -801,16 +811,16 @@ stream_start_error(StateData, Error) -> Lang :: ejabberd:lang()) -> any(). send_header(StateData, Server, Version, Lang) -> Header = mongoose_c2s_stanzas:stream_header(Server, Version, Lang, StateData#c2s_data.streamid), - send_text(StateData, Header). + send_xml(StateData, Header). send_trailer(StateData) -> - send_text(StateData, ?STREAM_TRAILER). + send_xml(StateData, ?XML_STREAM_TRAILER). -spec c2s_stream_error(c2s_data(), exml:element()) -> fsm_res(). c2s_stream_error(StateData, Error) -> ?LOG_DEBUG(#{what => c2s_stream_error, xml_error => Error, c2s_state => StateData}), send_element_from_server_jid(StateData, Error), - send_text(StateData, ?STREAM_TRAILER), + send_xml(StateData, ?XML_STREAM_TRAILER), {stop, {shutdown, stream_error}, StateData}. -spec send_element_from_server_jid(c2s_data(), exml:element()) -> any(). @@ -830,9 +840,6 @@ bounce_messages(StateData) -> {route, Acc} -> reroute(StateData, Acc), bounce_messages(StateData); - {route, _From, _To, Acc} -> - reroute(StateData, Acc), - bounce_messages(StateData); _ -> bounce_messages(StateData) after 0 -> ok @@ -886,8 +893,8 @@ send_element(StateData, El, Acc) -> send_element(StateData, [El], Acc). -spec send_xml(c2s_data(), exml_stream:element() | [exml_stream:element()]) -> maybe_ok(). -send_xml(StateData, Xml) -> - send_text(StateData, exml:to_iolist(Xml)). +send_xml(#c2s_data{socket = Socket}, Xml) -> + mongoose_c2s_socket:send_xml(Socket, Xml). state_timeout(#{c2s_state_timeout := Timeout}) -> {state_timeout, Timeout, state_timeout_termination}. @@ -917,7 +924,12 @@ hook_arg(StateData, C2SState, EventType, EventContent, Reason) -> %%% API %%%---------------------------------------------------------------------- --spec start_link({ranch:ref(), ranch_tcp, mongoose_listener:options()}, [gen_statem:start_opt()]) -> +-spec start({module(), term(), listener_opts()}, [gen_statem:start_opt()]) -> + supervisor:startchild_ret(). +start(Params, ProcOpts) -> + supervisor:start_child(mongoose_c2s_sup, [Params, ProcOpts]). + +-spec start_link({module(), term(), listener_opts()}, [gen_statem:start_opt()]) -> gen_statem:start_ret(). start_link(Params, ProcOpts) -> gen_statem:start_link(?MODULE, Params, ProcOpts). @@ -926,10 +938,14 @@ start_link(Params, ProcOpts) -> stop(Pid, Reason) -> gen_statem:cast(Pid, {stop, Reason}). --spec exit(pid(), binary()) -> ok. +-spec exit(pid(), binary() | atom()) -> ok. exit(Pid, Reason) -> gen_statem:cast(Pid, {exit, Reason}). +-spec async(pid(), fun(), [term()]) -> ok. +async(Pid, Fun, Args) -> + gen_statem:cast(Pid, {async, Fun, Args}). + -spec get_host_type(c2s_data()) -> mongooseim:host_type(). get_host_type(#c2s_data{host_type = HostType}) -> HostType. @@ -946,7 +962,7 @@ get_sid(#c2s_data{sid = Sid}) -> get_ip(#c2s_data{socket = Socket}) -> mongoose_c2s_socket:get_ip(Socket). --spec get_socket(c2s_data()) -> term(). +-spec get_socket(c2s_data()) -> mongoose_c2s_socket:socket() | undefined. get_socket(#c2s_data{socket = Socket}) -> Socket. @@ -962,9 +978,12 @@ get_lang(#c2s_data{lang = Lang}) -> get_stream_id(#c2s_data{streamid = StreamId}) -> StreamId. --spec get_mod_state(c2s_data(), atom()) -> term() | {error, not_found}. +-spec get_mod_state(c2s_data(), atom()) -> {ok, term()} | {error, not_found}. get_mod_state(#c2s_data{state_mod = Handlers}, HandlerName) -> - maps:get(HandlerName, Handlers, {error, not_found}). + case maps:get(HandlerName, Handlers, undefined) of + undefined -> {error, not_found}; + HandlerState -> {ok, HandlerState} + end. -spec merge_mod_state(c2s_data(), map()) -> c2s_data(). merge_mod_state(StateData = #c2s_data{state_mod = StateHandlers}, MoreHandlers) -> diff --git a/src/c2s/mongoose_c2s_listener.erl b/src/c2s/mongoose_c2s_listener.erl index b7c3aedec6c..2cb3baa51b6 100644 --- a/src/c2s/mongoose_c2s_listener.erl +++ b/src/c2s/mongoose_c2s_listener.erl @@ -48,7 +48,7 @@ handle_user_open_session(Acc, #{c2s_data := StateData}, #{host_type := HostType, %% ranch_protocol start_link(Ref, Transport, Opts = #{hibernate_after := HibernateAfterTimeout}) -> - mongoose_c2s:start_link({Ref, Transport, Opts}, [{hibernate_after, HibernateAfterTimeout}]). + mongoose_c2s:start_link({mongoose_c2s_ranch, {Transport, Ref}, Opts}, [{hibernate_after, HibernateAfterTimeout}]). %% supervisor -spec start_link(options()) -> any(). diff --git a/src/c2s/mongoose_c2s_ranch.erl b/src/c2s/mongoose_c2s_ranch.erl new file mode 100644 index 00000000000..a14a7c0908b --- /dev/null +++ b/src/c2s/mongoose_c2s_ranch.erl @@ -0,0 +1,138 @@ +-module(mongoose_c2s_ranch). +-behaviour(mongoose_c2s_socket). + +-export([socket_new/2, + socket_peername/1, + tcp_to_tls/2, + socket_handle_data/2, + socket_activate/1, + socket_close/1, + socket_send_xml/2, + has_peer_cert/2, + is_channel_binding_supported/1, + is_ssl/1]). + +-record(state, { + transport :: transport(), + ranch_ref :: ranch:ref(), + socket :: ranch_transport:socket(), + ip :: {inet:ip_address(), inet:port_number()} + }). + +-type state() :: #state{}. +-type transport() :: ranch_tcp | ranch_ssl | fast_tls. + +-spec socket_new(term(), mongoose_listener:options()) -> state(). +socket_new({ranch_tcp, RanchRef}, #{proxy_protocol := true}) -> + {ok, #{src_address := PeerIp, src_port := PeerPort}} = ranch:recv_proxy_header(RanchRef, 1000), + {ok, TcpSocket} = ranch:handshake(RanchRef), + #state{ + transport = ranch_tcp, + ranch_ref = RanchRef, + socket = TcpSocket, + ip = {PeerIp, PeerPort}}; +socket_new({ranch_tcp, RanchRef}, #{proxy_protocol := false}) -> + {ok, TcpSocket} = ranch:handshake(RanchRef), + {ok, Ip} = ranch_tcp:peername(TcpSocket), + #state{ + transport = ranch_tcp, + ranch_ref = RanchRef, + socket = TcpSocket, + ip = Ip}. + +-spec socket_peername(state()) -> {inet:ip_address(), inet:port_number()}. +socket_peername(#state{ip = Ip}) -> + Ip. + +-spec tcp_to_tls(state(), mongoose_listener:options()) -> + {ok, state()} | {error, term()}. +tcp_to_tls(#state{socket = TcpSocket} = State, #{tls := #{module := TlsMod, opts := TlsOpts}}) -> + ranch_tcp:setopts(TcpSocket, [{active, false}]), + case tcp_to_tls(TlsMod, TcpSocket, TlsOpts) of + {ok, TlsSocket} -> + {ok, State#state{transport = TlsMod, socket = TlsSocket}}; + {error, Reason} -> + {error, Reason} + end. + +tcp_to_tls(fast_tls, TcpSocket, TlsOpts) -> + case fast_tls:tcp_to_tls(TcpSocket, TlsOpts) of + {ok, TlsSocket} -> + fast_tls:recv_data(TlsSocket, <<>>), + {ok, TlsSocket}; + Other -> Other + end; +tcp_to_tls(just_tls, TcpSocket, TlsOpts) -> + case ranch_ssl:handshake(TcpSocket, TlsOpts, 1000) of + {ok, TlsSocket, _} -> {ok, TlsSocket}; + Other -> Other + end. + +-spec socket_handle_data(state(), {tcp | ssl, term(), iodata()}) -> + iodata() | {raw, [exml:element()]} | {error, term()}. +socket_handle_data(#state{transport = fast_tls, socket = TlsSocket}, {tcp, _Socket, Data}) -> + mongoose_metrics:update(global, [data, xmpp, received, encrypted_size], iolist_size(Data)), + case fast_tls:recv_data(TlsSocket, Data) of + {ok, DecryptedData} -> + DecryptedData; + {error, Reason} -> + {error, Reason} + end; +socket_handle_data(#state{transport = ranch_ssl, socket = Socket}, {ssl, Socket, Data}) -> + mongoose_metrics:update(global, [data, xmpp, received, encrypted_size], iolist_size(Data)), + Data; +socket_handle_data(#state{transport = ranch_tcp, socket = Socket}, {tcp, Socket, Data}) -> + Data. + +-spec socket_activate(state()) -> ok. +socket_activate(#state{transport = fast_tls, socket = Socket}) -> + fast_tls:setopts(Socket, [{active, once}]); +socket_activate(#state{transport = ranch_ssl, socket = Socket}) -> + ranch_ssl:setopts(Socket, [{active, once}]); +socket_activate(#state{transport = ranch_tcp, socket = Socket}) -> + ranch_tcp:setopts(Socket, [{active, once}]). + +-spec socket_close(state()) -> ok. +socket_close(#state{transport = fast_tls, socket = Socket}) -> + fast_tls:close(Socket); +socket_close(#state{transport = ranch_ssl, socket = Socket}) -> + ranch_ssl:close(Socket); +socket_close(#state{transport = ranch_tcp, socket = Socket}) -> + ranch_tcp:close(Socket). + +-spec socket_send_xml(state(), iodata() | exml_stream:element() | [exml_stream:element()]) -> + ok | {error, term()}. +socket_send_xml(#state{transport = Transport, socket = Socket}, XML) -> + Text = exml:to_iolist(XML), + case Transport:send(Socket, Text) of + ok -> + mongoose_metrics:update(global, [data, xmpp, sent, xml_stanza_size], iolist_size(Text)), + ok; + Error -> + Error + end. + +-spec has_peer_cert(mongoose_c2s_socket:state(), mongoose_listener:options()) -> boolean(). +has_peer_cert(#state{transport = fast_tls, socket = Socket}, #{tls := TlsOpts}) -> + case {fast_tls:get_verify_result(Socket), fast_tls:get_peer_certificate(Socket), TlsOpts} of + {0, {ok, _}, _} -> true; + %% 18 is OpenSSL's and fast_tls's error code for self-signed certs + {18, {ok, _}, #{verify_mode := selfsigned_peer}} -> true; + {_, {ok, _}, _} -> false; + {_, error, _} -> false + end; +has_peer_cert(#state{transport = ranch_ssl, socket = Socket}, _) -> + case ssl:peercert(Socket) of + {ok, _PeerCert} -> true; + _ -> false + end; +has_peer_cert(#state{transport = ranch_tcp}, _) -> + false. + +-spec is_channel_binding_supported(mongoose_c2s_socket:state()) -> boolean(). +is_channel_binding_supported(#state{transport = Transport}) -> + fast_tls == Transport. + +-spec is_ssl(mongoose_c2s_socket:state()) -> boolean(). +is_ssl(#state{transport = Transport}) -> + ranch_tcp /= Transport. diff --git a/src/c2s/mongoose_c2s_socket.erl b/src/c2s/mongoose_c2s_socket.erl index 4aaad1fb042..e9ec150830a 100644 --- a/src/c2s/mongoose_c2s_socket.erl +++ b/src/c2s/mongoose_c2s_socket.erl @@ -2,55 +2,66 @@ -include("mongoose_logger.hrl"). --export([new_socket/2, - tcp_to_tls/2, - handle_socket_data/2, - activate_socket/1, +-export([new/3, + handle_data/2, + activate/1, close/1, - send_text/2, - has_peer_cert/2, is_channel_binding_supported/1, + has_peer_cert/2, + tcp_to_tls/2, is_ssl/1, - get_ip/1, - get_conn_type/1 - ]). - --record(c2s_socket, { - transport :: transport(), - socket :: ranch_transport:socket(), - ip :: undefined | {inet:ip_address(), inet:port_number()}, - ranch_ref :: ranch:ref() - }). + send_xml/2]). + +-export([get_ip/1, + get_transport/1, + get_conn_type/1]). + +-callback socket_new(term(), mongoose_c2s:listener_opts()) -> state(). +-callback socket_peername(state()) -> {inet:ip_address(), inet:port_number()}. +-callback tcp_to_tls(state(), mongoose_c2s:listener_opts()) -> + {ok, state()} | {error, term()}. +-callback socket_handle_data(state(), {tcp | ssl, term(), iodata()}) -> + iodata() | {raw, [exml:element()]} | {error, term()}. +-callback socket_activate(state()) -> ok. +-callback socket_close(state()) -> ok. +-callback socket_send_xml(state(), iodata() | exml_stream:element() | [exml_stream:element()]) -> + ok | {error, term()}. +-callback has_peer_cert(mongoose_c2s_socket:state(), mongoose_c2s:listener_opts()) -> boolean(). +-callback is_channel_binding_supported(mongoose_c2s_socket:state()) -> boolean(). +-callback is_ssl(mongoose_c2s_socket:state()) -> boolean(). + +-record(c2s_socket, {module :: module(), + state :: state()}). -type socket() :: #c2s_socket{}. --type transport() :: ranch_tcp | ranch_ssl | fast_tls. --type conntype() :: c2s | c2s_tls. --export_type([transport/0, socket/0, conntype/0]). +-type state() :: term(). +-type conn_type() :: c2s | c2s_tls. +-export_type([socket/0, state/0, conn_type/0]). + +-spec new(module(), term(), mongoose_listener:options()) -> socket(). +new(Module, SocketOpts, LOpts) -> + State = Module:socket_new(SocketOpts, LOpts), + PeerIp = Module:socket_peername(State), + verify_ip_is_not_blacklisted(PeerIp), + C2SSocket = #c2s_socket{ + module = Module, + state = State}, + handle_socket_and_ssl_config(C2SSocket, LOpts). -%%%---------------------------------------------------------------------- -%%% socket helpers -%%%---------------------------------------------------------------------- +verify_ip_is_not_blacklisted(PeerIp) -> + case mongoose_hooks:check_bl_c2s(PeerIp) of + true -> + ?LOG_INFO(#{what => c2s_blacklisted_ip, ip => PeerIp, + text => <<"Connection attempt from blacklisted IP">>}), + throw({stop, {shutdown, ip_blacklisted}}); + false -> + ok + end. --spec new_socket(ranch:ref(), mongoose_listener:options()) -> socket(). -new_socket(RanchRef, Opts = #{proxy_protocol := true}) -> - {ok, #{src_address := PeerIp, src_port := PeerPort}} = ranch:recv_proxy_header(RanchRef, 1000), - verify_ip_is_not_blacklisted(PeerIp), - {ok, TcpSocket} = ranch:handshake(RanchRef), - Ip = {PeerIp, PeerPort}, - handle_socket_and_ssl_config(RanchRef, Opts, TcpSocket, Ip); -new_socket(RanchRef, Opts = #{proxy_protocol := false}) -> - {ok, TcpSocket} = ranch:handshake(RanchRef), - {ok, {PeerIp, _PeerPort} = Ip} = ranch_tcp:peername(TcpSocket), - verify_ip_is_not_blacklisted(PeerIp), - handle_socket_and_ssl_config(RanchRef, Opts, TcpSocket, Ip). - -handle_socket_and_ssl_config( - RanchRef, #{tls := #{mode := tls, module := TlsMod, opts := TlsOpts}}, TcpSocket, Ip) -> - case tcp_to_tls(TlsMod, TcpSocket, TlsOpts) of - {ok, TlsSocket} -> - C2SSocket = #c2s_socket{transport = TlsMod, socket = TlsSocket, - ip = Ip, ranch_ref = RanchRef}, - activate_socket(C2SSocket), - C2SSocket; +handle_socket_and_ssl_config(C2SSocket, #{tls := #{mode := tls}} = LOpts) -> + case tcp_to_tls(C2SSocket, LOpts) of + {ok, TlsC2SSocket} -> + activate(TlsC2SSocket), + TlsC2SSocket; {error, closed} -> throw({stop, {shutdown, tls_closed}}); {error, timeout} -> @@ -58,124 +69,62 @@ handle_socket_and_ssl_config( {error, {tls_alert, TlsAlert}} -> throw({stop, TlsAlert}) end; -handle_socket_and_ssl_config(RanchRef, _Opts, TcpSocket, Ip) -> - C2SSocket = #c2s_socket{transport = ranch_tcp, socket = TcpSocket, - ip = Ip, ranch_ref = RanchRef}, - activate_socket(C2SSocket), +handle_socket_and_ssl_config(C2SSocket, _Opts) -> + activate(C2SSocket), C2SSocket. -spec tcp_to_tls(socket(), mongoose_listener:options()) -> {ok, socket()} | {error, term()}. -tcp_to_tls(#c2s_socket{transport = ranch_tcp, socket = TcpSocket} = C2SSocket, - #{tls := #{module := TlsMod, opts := TlsOpts}}) -> - ranch_tcp:setopts(TcpSocket, [{active, false}]), - case tcp_to_tls(TlsMod, TcpSocket, TlsOpts) of - {ok, TlsSocket} -> - C2SSocket1 = C2SSocket#c2s_socket{transport = TlsMod, socket = TlsSocket}, - activate_socket(C2SSocket1), - {ok, C2SSocket1}; - {error, Reason} -> - {error, Reason} - end; -tcp_to_tls(_, _) -> - {error, already_tls_connection}. - -tcp_to_tls(fast_tls, TcpSocket, TlsOpts) -> - case fast_tls:tcp_to_tls(TcpSocket, TlsOpts) of - {ok, TlsSocket} -> - fast_tls:recv_data(TlsSocket, <<>>), - {ok, TlsSocket}; - Other -> Other - end; -tcp_to_tls(just_tls, TcpSocket, TlsOpts) -> - case ranch_ssl:handshake(TcpSocket, TlsOpts, 1000) of - {ok, TlsSocket, _} -> {ok, TlsSocket}; - Other -> Other +tcp_to_tls(#c2s_socket{module = Module, state = State} = C2SSocket, LOpts) -> + case Module:tcp_to_tls(State, LOpts) of + {ok, NewState} -> + {ok, C2SSocket#c2s_socket{state = NewState}}; + Error -> + Error end. -verify_ip_is_not_blacklisted(PeerIp) -> - case mongoose_hooks:check_bl_c2s(PeerIp) of - true -> - ?LOG_INFO(#{what => c2s_blacklisted_ip, ip => PeerIp, - text => <<"Connection attempt from blacklisted IP">>}), - throw({stop, {shutdown, ip_blacklisted}}); - false -> - ok - end. - --spec handle_socket_data(socket(), {tcp | ssl, term(), iodata()}) -> - iodata() | {error, term()}. -handle_socket_data(#c2s_socket{transport = fast_tls, socket = TlsSocket}, {tcp, _Socket, Data}) -> - mongoose_metrics:update(global, [data, xmpp, received, encrypted_size], iolist_size(Data)), - case fast_tls:recv_data(TlsSocket, Data) of - {ok, DecryptedData} -> - DecryptedData; - {error, Reason} -> - {error, Reason} - end; -handle_socket_data(#c2s_socket{transport = ranch_ssl, socket = Socket}, {ssl, Socket, Data}) -> - mongoose_metrics:update(global, [data, xmpp, received, encrypted_size], iolist_size(Data)), - Data; -handle_socket_data(#c2s_socket{transport = ranch_tcp, socket = Socket}, {tcp, Socket, Data}) -> - Data; -handle_socket_data(_, _) -> +-spec handle_data(socket(), {tcp | ssl, term(), iodata()}) -> + iodata() | {raw, [term()]} | {error, term()}. +handle_data(#c2s_socket{module = Module, state = State}, Payload) -> + Module:socket_handle_data(State, Payload); +handle_data(_, _) -> {error, bad_packet}. --spec activate_socket(socket()) -> ok | {error, term()}. -activate_socket(#c2s_socket{transport = fast_tls, socket = Socket}) -> - fast_tls:setopts(Socket, [{active, once}]); -activate_socket(#c2s_socket{transport = ranch_ssl, socket = Socket}) -> - ranch_ssl:setopts(Socket, [{active, once}]); -activate_socket(#c2s_socket{transport = ranch_tcp, socket = Socket}) -> - ranch_tcp:setopts(Socket, [{active, once}]). +-spec activate(socket()) -> ok | {error, term()}. +activate(#c2s_socket{module = Module, state = State}) -> + Module:socket_activate(State). -spec close(socket()) -> ok. -close(#c2s_socket{transport = fast_tls, socket = Socket}) when Socket =/= undefined -> - fast_tls:close(Socket); -close(#c2s_socket{transport = ranch_ssl, socket = Socket}) when Socket =/= undefined -> - ranch_ssl:close(Socket); -close(#c2s_socket{transport = ranch_tcp, socket = Socket}) when Socket =/= undefined -> - ranch_tcp:close(Socket); -close(_) -> - ok. - --spec send_text(socket(), iodata()) -> ok | {error, term()}. -send_text(#c2s_socket{transport = fast_tls, socket = Socket}, Text) -> - fast_tls:send(Socket, Text); -send_text(#c2s_socket{transport = ranch_ssl, socket = Socket}, Text) -> - ranch_ssl:send(Socket, Text); -send_text(#c2s_socket{transport = ranch_tcp, socket = Socket}, Text) -> - ranch_tcp:send(Socket, Text). +close(#c2s_socket{module = Module, state = State}) -> + Module:socket_close(State). + +-spec send_xml(socket(), exml_stream:element() | [exml_stream:element()]) -> ok | {error, term()}. +send_xml(#c2s_socket{module = Module, state = State}, XML) -> + Module:socket_send_xml(State, XML). %% 18 is OpenSSL's and fast_tls's error code for self-signed certs -spec has_peer_cert(socket(), mongoose_listener:options()) -> boolean(). -has_peer_cert(#c2s_socket{transport = fast_tls, socket = Socket}, #{tls := TlsOpts}) -> - case {fast_tls:get_verify_result(Socket), fast_tls:get_peer_certificate(Socket), TlsOpts} of - {0, {ok, _}, _} -> true; - {18, {ok, _}, #{verify_mode := selfsigned_peer}} -> true; - {_, {ok, _}, _} -> false; - {_, error, _} -> false - end; -has_peer_cert(#c2s_socket{transport = ranch_ssl, socket = Socket}, _) -> - case ssl:peercert(Socket) of - {ok, _PeerCert} -> true; - _ -> false - end; -has_peer_cert(#c2s_socket{transport = ranch_tcp}, _) -> - false. +has_peer_cert(#c2s_socket{module = Module, state = State}, LOpts) -> + Module:has_pert_cert(State, LOpts). -spec is_channel_binding_supported(socket()) -> boolean(). -is_channel_binding_supported(#c2s_socket{transport = Transport}) -> - fast_tls =:= Transport. - --spec is_ssl(socket()) -> term(). -is_ssl(#c2s_socket{transport = Transport}) -> - ranch_tcp =/= Transport. +is_channel_binding_supported(#c2s_socket{module = Module, state = State}) -> + Module:is_channel_binding_supported(State). + +-spec is_ssl(socket()) -> boolean(). +is_ssl(#c2s_socket{module = Module, state = State}) -> + Module:is_ssl(State). + +-spec get_transport(socket()) -> module(). +get_transport(#c2s_socket{module = Module}) -> + Module. + +-spec get_conn_type(socket()) -> conn_type(). +get_conn_type(Socket) -> + case is_ssl(Socket) of + true -> c2s_tls; + false -> c2s + end. -spec get_ip(socket()) -> term(). -get_ip(#c2s_socket{ip = Ip}) -> - Ip. - --spec get_conn_type(socket()) -> conntype(). -get_conn_type(#c2s_socket{transport = ranch_tcp}) -> c2s; -get_conn_type(#c2s_socket{transport = ranch_ssl}) -> c2s_tls; -get_conn_type(#c2s_socket{transport = fast_tls}) -> c2s_tls. +get_ip(#c2s_socket{module = Module, state = State}) -> + Module:socket_peername(State). diff --git a/src/c2s/mongoose_c2s_stanzas.erl b/src/c2s/mongoose_c2s_stanzas.erl index 9e83b016d16..09b06bc146e 100644 --- a/src/c2s/mongoose_c2s_stanzas.erl +++ b/src/c2s/mongoose_c2s_stanzas.erl @@ -1,5 +1,6 @@ -module(mongoose_c2s_stanzas). +-include_lib("exml/include/exml_stream.hrl"). -include("jlib.hrl"). -export([ @@ -14,22 +15,22 @@ successful_session_establishment/1 ]). +-spec stream_header(binary(), binary(), binary(), binary()) -> exml_stream:start(). stream_header(Server, Version, Lang, StreamId) -> - VersionStr = case Version of - <<>> -> <<>>; - _ -> <<" version='", (Version)/binary, "'">> - end, - LangStr = case Lang of - <<>> -> <<>>; - _ when is_binary(Lang) -> <<" xml:lang='", (Lang)/binary, "'">> - end, - <<"", - "">>. + Attrs = [{<<"xmlns">>, ?NS_CLIENT}, + {<<"xmlns:stream">>, <<"http://etherx.jabber.org/streams">>}, + {<<"id">>, StreamId}, + {<<"from">>, Server}], + Attrs1 = case Version of + <<>> -> Attrs; + _ -> [{<<"version">>, Version} | Attrs] + end, + Attrs2 = case Lang of + <<>> -> Attrs1; + _ -> [{<<"xml:lang">>, Lang} | Attrs1] + end, + #xmlstreamstart{name = <<"stream:stream">>, + attrs = Attrs2}. -spec stream_features([exml:element() | exml:cdata()]) -> exml:element(). stream_features(Features) -> diff --git a/src/ejabberd_app.erl b/src/ejabberd_app.erl index dfea351e799..cdb18b6f51d 100644 --- a/src/ejabberd_app.erl +++ b/src/ejabberd_app.erl @@ -82,10 +82,11 @@ start(_, _) -> %% before shutting down the processes of the application. prep_stop(State) -> mongoose_deprecations:stop(), + broadcast_c2s_shutdown_listeners(), mongoose_listener:stop(), mongoose_modules:stop(), mongoose_service:stop(), - broadcast_c2s_shutdown(), + broadcast_c2s_shutdown_sup(), mongoose_wpool:stop(), mongoose_metrics:remove_all_metrics(), mongoose_config:stop(), @@ -115,18 +116,36 @@ db_init() -> end, mnesia:wait_for_tables(mnesia:system_info(local_tables), infinity). --spec broadcast_c2s_shutdown() -> 'ok'. -broadcast_c2s_shutdown() -> - Children = supervisor:which_children(ejabberd_c2s_sup), +-spec broadcast_c2s_shutdown_listeners() -> ok. +broadcast_c2s_shutdown_listeners() -> + Children = supervisor:which_children(mongoose_listener_sup), + Listeners = [Ref || {Ref, _, _, [mongoose_c2s_listener]} <- Children], lists:foreach( - fun({_, C2SPid, _, _}) -> - C2SPid ! system_shutdown - end, Children), + fun(Listener) -> + ranch:suspend_listener(Listener), + [mongoose_c2s:exit(Pid, system_shutdown) || Pid <- ranch:procs(Listener, connections)], + mongoose_lib:wait_until( + fun() -> + length(ranch:procs(Listener, connections)) + end, + 0) + end, + Listeners). + +-spec broadcast_c2s_shutdown_sup() -> ok. +broadcast_c2s_shutdown_sup() -> + Children = supervisor:which_children(mongoose_c2s_sup), + lists:foreach( + fun({_, Pid, _, _}) -> + mongoose_c2s:exit(Pid, system_shutdown) + end, + Children), mongoose_lib:wait_until( - fun() -> - Res = supervisor:count_children(ejabberd_c2s_sup), + fun() -> + Res = supervisor:count_children(mongoose_c2s_sup), proplists:get_value(active, Res) - end, 0). + end, + 0). %%% %%% PID file diff --git a/src/ejabberd_c2s.erl b/src/ejabberd_c2s.erl deleted file mode 100644 index 5deb14fd4d9..00000000000 --- a/src/ejabberd_c2s.erl +++ /dev/null @@ -1,3340 +0,0 @@ -%%%---------------------------------------------------------------------- -%%% File : ejabberd_c2s.erl -%%% Author : Alexey Shchepin -%%% Purpose : Serve C2S connection -%%% Created : 16 Nov 2002 by Alexey Shchepin -%%% -%%% -%%% ejabberd, Copyright (C) 2002-2011 ProcessOne -%%% -%%% This program is free software; you can redistribute it and/or -%%% modify it under the terms of the GNU General Public License as -%%% published by the Free Software Foundation; either version 2 of the -%%% License, or (at your option) any later version. -%%% -%%% This program is distributed in the hope that it will be useful, -%%% but WITHOUT ANY WARRANTY; without even the implied warranty of -%%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -%%% General Public License for more details. -%%% -%%% You should have received a copy of the GNU General Public License -%%% along with this program; if not, write to the Free Software -%%% Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA -%%% -%%%---------------------------------------------------------------------- - --module(ejabberd_c2s). --author('alexey@process-one.net'). --update_info({update, 0}). -%% External exports --export([start/2, - stop/1, - start_link/2, - send_text/2, - get_presence/1, - get_aux_field/2, - set_aux_field/3, - del_aux_field/2, - get_subscription/2, - get_subscribed/1, - send_filtered/5, - store_session_info/4, - remove_session_info/3, - get_info/1, - run_remote_hook/3, - run_remote_hook_after/4]). - -%% mongoose_listener API --export([socket_type/0, - start_listener/1]). - -%% gen_fsm callbacks --export([init/1, - wait_for_stream/2, - wait_for_feature_before_auth/2, - wait_for_feature_after_auth/2, - wait_for_session_or_sm/2, - wait_for_sasl_response/2, - session_established/2, session_established/3, - resume_session/2, resume_session/3, - handle_event/3, - handle_sync_event/4, - code_change/4, - handle_info/3, - terminate/4, - print_state/1]). - --ignore_xref([del_aux_field/2, get_info/1, print_state/1, resume_session/2, - resume_session/3, send_text/2, session_established/2, session_established/3, - socket_type/0, start_link/2, wait_for_feature_after_auth/2, - wait_for_feature_before_auth/2, wait_for_sasl_response/2, - wait_for_session_or_sm/2, wait_for_stream/2]). - --include("mongoose.hrl"). --include("ejabberd_c2s.hrl"). --include("jlib.hrl"). --include_lib("exml/include/exml.hrl"). --xep([{xep, 18}, {version, "0.2"}]). --behaviour(p1_fsm_old). --behaviour(mongoose_listener). - --export_type([broadcast/0, packet/0, state/0]). - --type packet() :: {jid:jid(), jid:jid(), exml:element()}. --type socket_data() :: {ejabberd:sockmod(), term()}. --type start_result() :: {error, _} - | {ok, undefined | pid()} - | {ok, undefined | pid(), _}. - --type options() :: #{access := atom(), - shaper := atom(), - hibernate_after := non_neg_integer(), - atom() => any()}. - -%%%---------------------------------------------------------------------- -%%% API -%%%---------------------------------------------------------------------- - --spec start_listener(options()) -> ok. -start_listener(Opts) -> - [ssl_crl_cache:insert({file, CRL}) || CRL <- crl_files(Opts)], - mongoose_tcp_listener:start_listener(Opts). - -crl_files(#{tls := #{crl_files := Files}}) -> Files; -crl_files(#{}) -> []. - --spec start(socket_data(), options()) -> start_result(). -start(SockData, Opts) -> - ?SUPERVISOR_START(SockData, Opts). - --spec start_link(socket_data(), options()) -> start_result(). -start_link(SockData, Opts) -> - p1_fsm_old:start_link(ejabberd_c2s, {SockData, Opts}, - ?FSMOPTS ++ fsm_limit_opts(Opts)). - --spec socket_type() -> mongoose_listener:socket_type(). -socket_type() -> - xml_stream. - -%% @doc Return Username, Resource and presence information -get_presence(FsmRef) -> - p1_fsm_old:sync_send_all_state_event(FsmRef, get_presence, 1000). - -get_info(FsmRef) -> - p1_fsm_old:sync_send_all_state_event(FsmRef, get_info, 5000). - - --spec get_aux_field(Key :: aux_key(), - State :: state()) -> 'error' | {'ok', aux_value()}. -get_aux_field(Key, #state{aux_fields = Opts}) -> - case lists:keyfind(Key, 1, Opts) of - {_, Val} -> - {ok, Val}; - _ -> - error - end. - - --spec set_aux_field(Key :: aux_key(), - Val :: aux_value(), - State :: state()) -> state(). -set_aux_field(Key, Val, #state{aux_fields = Opts} = State) -> - Opts1 = lists:keydelete(Key, 1, Opts), - State#state{aux_fields = [{Key, Val}|Opts1]}. - - --spec del_aux_field(Key :: aux_key(), State :: state()) -> aux_value(). -del_aux_field(Key, #state{aux_fields = Opts} = State) -> - Opts1 = lists:keydelete(Key, 1, Opts), - State#state{aux_fields = Opts1}. - - --spec get_subscription(From :: jid:jid(), State :: state()) -> - both | from | 'none' | 'to'. -get_subscription(From = #jid{}, StateData) -> - {LFrom, LBFrom} = lowcase_and_bare(From), - F = is_subscribed_to_my_presence(LFrom, LBFrom, StateData), - T = am_i_subscribed_to_presence(LFrom, LBFrom, StateData), - case {F, T} of - {true, true} -> both; - {true, _} -> from; - {_, true} -> to; - _ -> none - end. - -send_filtered(FsmRef, Feature, From, To, Packet) -> - FsmRef ! {send_filtered, Feature, From, To, Packet}. - -%% @doc Stops the session gracefully, entering resume state if applicable -stop(FsmRef) -> - p1_fsm_old:send_event(FsmRef, closed). - -store_session_info(FsmRef, JID, Key, Value) -> - FsmRef ! {store_session_info, JID, Key, Value, self()}. - -remove_session_info(FsmRef, JID, Key) -> - FsmRef ! {remove_session_info, JID, Key, self()}. - -run_remote_hook(Pid, HandlerName, Args) -> - Pid ! {run_remote_hook, HandlerName, Args}. - -run_remote_hook_after(Delay, Pid, HandlerName, Args) -> - erlang:send_after(Delay, Pid, {run_remote_hook, HandlerName, Args}). - -%%%---------------------------------------------------------------------- -%%% Callback functions from gen_fsm -%%%---------------------------------------------------------------------- - --spec init({socket_data(), options()}) -> - {stop, normal} | {ok, wait_for_stream, state(), non_neg_integer()}. -init({{SockMod, Socket}, Opts}) -> - #{access := Access, shaper := Shaper, hibernate_after := HibernateAfter} = Opts, - XMLSocket = maps:get(xml_socket, Opts, false), - Zlib = zlib(Opts), - TLSOptions = tls_options(Opts), - TLSMode = tls_mode(Opts), - IP = peerip(SockMod, Socket), - case is_ip_blacklisted(IP) of - true -> - ?LOG_INFO(#{what => c2s_blacklisted_ip, ip => IP, - text => <<"Connection attempt from blacklisted IP">>}), - {stop, normal}; - false -> - Socket1 = maybe_start_tls(SockMod, Socket, TLSMode, TLSOptions), - SocketMonitor = mongoose_transport:monitor(SockMod, Socket1), - CredOpts = mongoose_credentials:make_opts(Opts), - {ok, wait_for_stream, #state{server = ?MYNAME, - socket = Socket1, - sockmod = SockMod, - socket_monitor = SocketMonitor, - xml_socket = XMLSocket, - zlib = Zlib, - tls_enabled = TLSMode =:= tls, - tls_options = TLSOptions, - tls_mode = TLSMode, - streamid = new_id(), - access = Access, - shaper = Shaper, - ip = IP, - lang = ?MYLANG, - hibernate_after= HibernateAfter, - cred_opts = CredOpts - }, - ?C2S_OPEN_TIMEOUT} - end. - -zlib(#{zlib := Limit}) -> {true, Limit}; -zlib(#{}) -> {false, 0}. - -tls_mode(#{tls := #{mode := Mode}}) -> Mode; -tls_mode(#{}) -> no_tls. - -tls_options(#{tls := TLS}) -> maps:without([mode, crl_files], TLS); -tls_options(#{}) -> undefined. - -maybe_start_tls(SockMod, Socket, tls, TLSOpts) -> - mongoose_transport:starttls(SockMod, Socket, TLSOpts); -maybe_start_tls(_SockMod, Socket, _Mode, _TLSOpts) -> - Socket. - -%% @doc Return list of all available resources of contacts, -get_subscribed(FsmRef) -> - p1_fsm_old:sync_send_all_state_event(FsmRef, get_subscribed, 1000). - -%%---------------------------------------------------------------------- -%% Func: StateName/2 -%%---------------------------------------------------------------------- - --spec wait_for_stream(Item :: ejabberd:xml_stream_item(), - StateData :: state()) -> fsm_return(). -wait_for_stream({xmlstreamstart, _Name, _} = StreamStart, StateData) -> - handle_stream_start(StreamStart, StateData); -wait_for_stream(timeout, StateData) -> - {stop, normal, StateData}; -wait_for_stream(closed, StateData) -> - {stop, normal, StateData}; -wait_for_stream(_UnexpectedItem, #state{server = Server} = StateData) -> - case mongoose_config:get_opt(hide_service_name, false) of - true -> - {stop, normal, StateData}; - false -> - send_header(StateData, Server, << "1.0">>, <<"">>), - c2s_stream_error(mongoose_xmpp_errors:xml_not_well_formed(), StateData) - end. - -handle_stream_start({xmlstreamstart, _Name, Attrs}, #state{} = S0) -> - Server = jid:nameprep(xml:get_attr_s(<<"to">>, Attrs)), - Lang = get_xml_lang(Attrs), - S1 = S0#state{server = Server, lang = Lang}, - case {xml:get_attr_s(<<"xmlns:stream">>, Attrs), - mongoose_domain_api:get_domain_host_type(Server)} of - {?NS_STREAM, {ok, HostType}} -> - StreamMgmtConfig = case gen_mod:is_loaded(HostType, mod_stream_management) of - true -> false; - _ -> disabled - end, - S = S1#state{host_type = HostType, stream_mgmt = StreamMgmtConfig}, - change_shaper(S, jid:make_noprep(<<>>, Server, <<>>)), - Version = xml:get_attr_s(<<"version">>, Attrs), - stream_start_by_protocol_version(Version, S); - {?NS_STREAM, {error, not_found}} -> - stream_start_error(mongoose_xmpp_errors:host_unknown(), S1); - {_InvalidNS, _} -> - stream_start_error(mongoose_xmpp_errors:invalid_namespace(), S1) - end. - -stream_start_error(Error, StateData) -> - send_header(StateData, ?MYNAME, <<"">>, ?MYLANG), - c2s_stream_error(Error, StateData). - --spec c2s_stream_error(Error, State) -> Result when - Error :: exml:element(), - State :: state(), - Result :: {stop, normal, state()}. -c2s_stream_error(Error, StateData) -> - ?LOG_DEBUG(#{what => c2s_stream_error, xml_error => Error, c2s_state => StateData}), - send_element_from_server_jid(StateData, Error), - send_trailer(StateData), - {stop, normal, StateData}. - -%% See RFC 6120 4.3.2: -%% -%% If the initiating entity includes in the initial stream header -%% the 'version' attribute set to a value of at least <<"1.0">> [...] -%% receiving entity MUST send a child element [...] -%% -%% (http://xmpp.org/rfcs/rfc6120.html#streams-negotiation-features) -stream_start_by_protocol_version(<<"1.0">>, #state{} = S) -> - stream_start_negotiate_features(S); -stream_start_by_protocol_version(_Pre1_0, S) -> - stream_start_error(mongoose_xmpp_errors:unsupported_version(), S). - -stream_start_negotiate_features(#state{} = S) -> - send_header(S, S#state.server, <<"1.0">>, ?MYLANG), - case {S#state.authenticated, S#state.resource} of - {false, _} -> - stream_start_features_before_auth(S); - {_, <<>>} -> - stream_start_features_after_auth(S); - {_, _} -> - send_element_from_server_jid(S, #xmlel{name = <<"stream:features">>}), - fsm_next_state(wait_for_session_or_sm, S) - end. - -stream_start_features_before_auth(#state{server = Server, - host_type = HostType, - cred_opts = CredOpts} = S) -> - Creds0 = mongoose_credentials:new(Server, HostType, CredOpts), - Creds = maybe_add_cert(Creds0, S), - SASLState = cyrsasl:server_new(<<"jabber">>, Server, HostType, <<>>, [], Creds), - SockMod = (S#state.sockmod):get_sockmod(S#state.socket), - send_element_from_server_jid(S, stream_features(determine_features(SockMod, S))), - fsm_next_state(wait_for_feature_before_auth, - S#state{sasl_state = SASLState}). - -stream_start_features_after_auth(#state{} = S) -> - SockMod = (S#state.sockmod):get_sockmod(S#state.socket), - Features = (maybe_compress_feature(SockMod, S) - ++ [#xmlel{name = <<"bind">>, - attrs = [{<<"xmlns">>, ?NS_BIND}]}, - #xmlel{name = <<"session">>, - attrs = [{<<"xmlns">>, ?NS_SESSION}]}] - ++ maybe_roster_versioning_feature(S) - ++ hook_enabled_features(S) ), - send_element_from_server_jid(S, stream_features(Features)), - fsm_next_state(wait_for_feature_after_auth, S). - -maybe_roster_versioning_feature(#state{host_type = HostType}) -> - mongoose_hooks:roster_get_versioning_feature(HostType). - -stream_features(FeatureElements) -> - #xmlel{name = <<"stream:features">>, - children = FeatureElements}. - -%% From RFC 6120, section 5.3.1: -%% -%% If TLS is mandatory-to-negotiate, the receiving entity SHOULD NOT -%% advertise support for any stream feature except STARTTLS during the -%% initial stage of the stream negotiation process, because further stream -%% features might depend on prior negotiation of TLS given the order of -%% layers in XMPP (e.g., the particular SASL mechanisms offered by the -%% receiving entity will likely depend on whether TLS has been negotiated). -%% -%% http://xmpp.org/rfcs/rfc6120.html#tls-rules-mtn -determine_features(SockMod, StateData) -> - OtherFeatures = maybe_compress_feature(SockMod, StateData) - ++ maybe_sasl_mechanisms(StateData) - ++ hook_enabled_features(StateData), - case can_use_tls(SockMod, StateData) of - true -> - case StateData#state.tls_mode of - starttls_required -> [starttls_stanza(required)]; - _ -> [starttls_stanza(optional)] ++ OtherFeatures - end; - false -> - OtherFeatures - end. - -maybe_compress_feature(SockMod, #state{zlib = {ZLib, _}}) -> - case can_use_zlib_compression(ZLib, SockMod) of - true -> [compression_zlib()]; - _ -> [] - end. - -maybe_sasl_mechanisms(#state{host_type = HostType} = S) -> - case cyrsasl:listmech(HostType) of - [] -> []; - Mechanisms -> - [#xmlel{name = <<"mechanisms">>, - attrs = [{<<"xmlns">>, ?NS_SASL}], - children = [ mechanism(M) || M <- Mechanisms, filter_mechanism(M, S) ]}] - end. - -hook_enabled_features(#state{host_type = HostType, server = LServer}) -> - mongoose_hooks:c2s_stream_features(HostType, LServer). - -starttls_stanza(TLSRequired) - when TLSRequired =:= required; - TLSRequired =:= optional -> - #xmlel{name = <<"starttls">>, - attrs = [{<<"xmlns">>, ?NS_TLS}], - children = [ #xmlel{name = <<"required">>} || TLSRequired =:= required ]}. - -can_use_tls(gen_tcp, #state{tls_mode = TLSMode, tls_enabled = false}) -> TLSMode =/= no_tls; -can_use_tls(_SockMod, _TLSOptions) -> false. - -can_use_zlib_compression(Zlib, SockMod) -> - Zlib andalso ( (SockMod == gen_tcp) orelse - (SockMod == mongoose_tls) ). - -compression_zlib() -> - #xmlel{name = <<"compression">>, - attrs = [{<<"xmlns">>, ?NS_FEATURE_COMPRESS}], - children = [#xmlel{name = <<"method">>, - children = [#xmlcdata{content = <<"zlib">>}]}]}. - -mechanism(S) -> - #xmlel{name = <<"mechanism">>, - children = [#xmlcdata{content = S}]}. - -filter_mechanism(<<"EXTERNAL">>, S) -> - case get_peer_cert(S) of - error -> false; - _ -> true - end; -filter_mechanism(<<"SCRAM-SHA-1-PLUS">>, S) -> - is_channel_binding_supported(S); -filter_mechanism(<<"SCRAM-SHA-", _N:3/binary, "-PLUS">>, S) -> - is_channel_binding_supported(S); -filter_mechanism(_, _) -> true. - -is_channel_binding_supported(State) -> - Socket = State#state.socket, - SockMod = (State#state.sockmod):get_sockmod(Socket), - is_fast_tls_configured(SockMod, Socket). - -is_fast_tls_configured(mongoose_tls, Socket) -> - fast_tls == mongoose_tls:get_sockmod(ejabberd_socket:get_socket(Socket)); -is_fast_tls_configured(_, _) -> - false. - -get_xml_lang(Attrs) -> - case xml:get_attr_s(<<"xml:lang">>, Attrs) of - Lang when size(Lang) =< 35 -> - %% As stated in BCP47, 4.4.1: - %% Protocols or specifications that - %% specify limited buffer sizes for - %% language tags MUST allow for - %% language tags of at least 35 characters. - Lang; - _ -> - %% Do not store long language tag to - %% avoid possible DoS/flood attacks - <<>> - end. - --spec get_peer_cert(state()) -> any() | error. -get_peer_cert(#state{socket = Socket, - sockmod = SockMod }) -> - case mongoose_transport:get_peer_certificate(SockMod, Socket) of - {ok, Cert} -> Cert; - _ -> error - end. - -maybe_add_cert(Creds, S) -> - case get_peer_cert(S) of - error -> Creds; - Cert -> mongoose_credentials:set(Creds, client_cert, Cert) - end. - --spec wait_for_feature_before_auth(Item :: ejabberd:xml_stream_item(), - State :: state()) -> fsm_return(). -wait_for_feature_before_auth({xmlstreamelement, - #xmlel{name = <<"enable">>} = El}, StateData) -> - maybe_unexpected_sm_request(wait_for_feature_before_auth, El, StateData); -wait_for_feature_before_auth({xmlstreamelement, El}, StateData) -> - #xmlel{name = Name, attrs = Attrs, children = Els} = El, - {Zlib, _} = StateData#state.zlib, - TLSEnabled = StateData#state.tls_enabled, - TLSMode = StateData#state.tls_mode, - SockMod = (StateData#state.sockmod):get_sockmod(StateData#state.socket), - CanUseTLS = can_use_tls(SockMod, StateData), - case {xml:get_attr_s(<<"xmlns">>, Attrs), Name} of - {?NS_SASL, <<"auth">>} when TLSEnabled orelse TLSMode =/= starttls_required -> - Mech = xml:get_attr_s(<<"mechanism">>, Attrs), - ClientIn = jlib:decode_base64(xml:get_cdata(Els)), - SaslState = StateData#state.sasl_state, - HostType = StateData#state.host_type, - AuthMech = [M || M <- cyrsasl:listmech(HostType), filter_mechanism(M, StateData)], - SocketData = #{socket => StateData#state.socket, auth_mech => AuthMech}, - StepResult = cyrsasl:server_start(SaslState, Mech, ClientIn, SocketData), - {NewFSMState, NewStateData} = handle_sasl_step(StateData, StepResult), - fsm_next_state(NewFSMState, NewStateData); - {?NS_TLS, <<"starttls">>} when CanUseTLS -> - TLSOpts = tls_options_with_certfile(StateData), - TLSSocket = mongoose_transport:starttls(StateData#state.sockmod, - StateData#state.socket, - TLSOpts, exml:to_binary(tls_proceed())), - fsm_next_state(wait_for_stream, - StateData#state{socket = TLSSocket, - streamid = new_id(), - tls_enabled = true - }); - {?NS_COMPRESS, <<"compress">>} when Zlib == true, - ((SockMod == gen_tcp) or - (SockMod == mongoose_tls)) -> - check_compression_auth(El, wait_for_feature_before_auth, StateData); - _ -> - terminate_when_tls_required_but_not_enabled(StateData, El) - end; -wait_for_feature_before_auth(timeout, StateData) -> - {stop, normal, StateData}; -wait_for_feature_before_auth({xmlstreamend, _Name}, StateData) -> - send_trailer(StateData), - {stop, normal, StateData}; -wait_for_feature_before_auth({xmlstreamerror, _}, StateData) -> - c2s_stream_error(mongoose_xmpp_errors:xml_not_well_formed(), StateData); -wait_for_feature_before_auth(closed, StateData) -> - {stop, normal, StateData}; -wait_for_feature_before_auth(_, StateData) -> - c2s_stream_error(mongoose_xmpp_errors:policy_violation(), StateData). - -tls_options_with_certfile(#state{host_type = HostType, tls_options = TLSOptions}) -> - case mongoose_config:lookup_opt([domain_certfile, HostType]) of - {ok, CertFile} -> TLSOptions#{certfile => CertFile}; - {error, not_found} -> TLSOptions - end. - -compressed() -> - #xmlel{name = <<"compressed">>, - attrs = [{<<"xmlns">>, ?NS_COMPRESS}]}. - -compress_unsupported_method() -> - #xmlel{name = <<"failure">>, - attrs = [{<<"xmlns">>, ?NS_COMPRESS}], - children = [#xmlel{name = <<"unsupported-method">>}]}. - -tls_proceed() -> - #xmlel{name = <<"proceed">>, - attrs = [{<<"xmlns">>, ?NS_TLS}]}. - -compress_setup_failed() -> - #xmlel{name = <<"failure">>, - attrs = [{<<"xmlns">>, ?NS_COMPRESS}], - children = [#xmlel{name = <<"setup-failed">>}]}. - --spec wait_for_sasl_response(Item :: ejabberd:xml_stream_item(), - State :: state()) -> fsm_return(). -wait_for_sasl_response({xmlstreamelement, - #xmlel{name = <<"enable">>} = El}, StateData) -> - maybe_unexpected_sm_request(wait_for_sasl_response, El, StateData); -wait_for_sasl_response({xmlstreamelement, El}, StateData) -> - #xmlel{name = Name, attrs = Attrs, children = Els} = El, - case {xml:get_attr_s(<<"xmlns">>, Attrs), Name} of - {?NS_SASL, <<"response">>} -> - ClientIn = jlib:decode_base64(xml:get_cdata(Els)), - StepResult = cyrsasl:server_step(StateData#state.sasl_state, ClientIn), - {NewFSMState, NewStateData} = handle_sasl_step(StateData, StepResult), - fsm_next_state(NewFSMState, NewStateData); - _ -> - process_unauthenticated_stanza(StateData, El), - fsm_next_state(wait_for_feature_before_auth, StateData) - end; -wait_for_sasl_response(timeout, StateData) -> - {stop, normal, StateData}; -wait_for_sasl_response({xmlstreamend, _Name}, StateData) -> - send_trailer(StateData), - {stop, normal, StateData}; -wait_for_sasl_response({xmlstreamerror, _}, StateData) -> - c2s_stream_error(mongoose_xmpp_errors:xml_not_well_formed(), StateData); -wait_for_sasl_response(closed, StateData) -> - {stop, normal, StateData}. - --spec wait_for_feature_after_auth(Item :: ejabberd:xml_stream_item(), - State :: state()) -> fsm_return(). -wait_for_feature_after_auth({xmlstreamelement, - #xmlel{name = <<"enable">>} = El}, StateData) -> - maybe_unexpected_sm_request(wait_for_feature_after_auth, El, StateData); -wait_for_feature_after_auth({xmlstreamelement, - #xmlel{name = <<"resume">>} = El}, StateData) -> - maybe_resume_session(wait_for_feature_after_auth, El, StateData); -wait_for_feature_after_auth({xmlstreamelement, El}, StateData) -> - case jlib:iq_query_info(El) of - #iq{type = set, xmlns = ?NS_BIND, sub_el = SubEl} = IQ -> - R1 = xml:get_path_s(SubEl, [{elem, <<"resource">>}, cdata]), - R = case jid:resourceprep(R1) of - error -> error; - <<>> -> generate_random_resource(); - Resource -> Resource - end, - case R of - error -> - Err = jlib:make_error_reply(El, mongoose_xmpp_errors:bad_request()), - send_element_from_server_jid(StateData, Err), - fsm_next_state(wait_for_feature_after_auth, StateData); - _ -> - JID = jid:replace_resource(StateData#state.jid, R), - JIDEl = #xmlel{name = <<"jid">>, - children = [#xmlcdata{content = jid:to_binary(JID)}]}, - Res = IQ#iq{type = result, - sub_el = [#xmlel{name = <<"bind">>, - attrs = [{<<"xmlns">>, ?NS_BIND}], - children = [JIDEl]}]}, - XmlEl = jlib:iq_to_xml(Res), - send_element_from_server_jid(StateData, XmlEl), - fsm_next_state(wait_for_session_or_sm, - StateData#state{resource = R, jid = JID}) - end; - _ -> - maybe_do_compress(El, wait_for_feature_after_auth, StateData) - end; - -wait_for_feature_after_auth(timeout, StateData) -> - {stop, normal, StateData}; - -wait_for_feature_after_auth({xmlstreamend, _Name}, StateData) -> - send_trailer(StateData), - {stop, normal, StateData}; - -wait_for_feature_after_auth({xmlstreamerror, _}, StateData) -> - c2s_stream_error(mongoose_xmpp_errors:xml_not_well_formed(), StateData); - -wait_for_feature_after_auth(closed, StateData) -> - {stop, normal, StateData}; - -wait_for_feature_after_auth(_, StateData) -> - c2s_stream_error(mongoose_xmpp_errors:policy_violation(), StateData). - --spec wait_for_session_or_sm(Item :: ejabberd:xml_stream_item(), - State :: state()) -> fsm_return(). -wait_for_session_or_sm({xmlstreamelement, - #xmlel{name = <<"enable">>} = El}, StateData) -> - maybe_enable_stream_mgmt(wait_for_session_or_sm, El, StateData); - -wait_for_session_or_sm({xmlstreamelement, - #xmlel{name = <<"r">>} = El}, StateData) -> - maybe_send_sm_ack(xml:get_tag_attr_s(<<"xmlns">>, El), - StateData#state.stream_mgmt, - StateData#state.stream_mgmt_in, - wait_for_session_or_sm, StateData); - -wait_for_session_or_sm({xmlstreamelement, El}, StateData0) -> - StateData = maybe_increment_sm_incoming(StateData0#state.stream_mgmt, - StateData0), - case jlib:iq_query_info(El) of - #iq{type = set, xmlns = ?NS_SESSION} -> - Acc = element_to_origin_accum(El, StateData0), - {Res, _Acc1, NStateData} = maybe_open_session(Acc, StateData), - case Res of - stop -> {stop, normal, NStateData}; - wait -> fsm_next_state(wait_for_session_or_sm, NStateData); - established -> fsm_next_state_pack(session_established, NStateData) - end; - _ -> - maybe_do_compress(El, wait_for_session_or_sm, StateData) - end; - -wait_for_session_or_sm(timeout, StateData) -> - {stop, normal, StateData}; - -wait_for_session_or_sm({xmlstreamend, _Name}, StateData) -> - send_trailer(StateData), - {stop, normal, StateData}; - -wait_for_session_or_sm({xmlstreamerror, _}, StateData) -> - c2s_stream_error(mongoose_xmpp_errors:xml_not_well_formed(), StateData); - -wait_for_session_or_sm(closed, StateData) -> - {stop, normal, StateData}; - -wait_for_session_or_sm(_, StateData) -> - c2s_stream_error(mongoose_xmpp_errors:policy_violation(), StateData). - -maybe_do_compress(El = #xmlel{name = Name, attrs = Attrs}, NextState, StateData) -> - SockMod = (StateData#state.sockmod):get_sockmod(StateData#state.socket), - {Zlib, _} = StateData#state.zlib, - case {xml:get_attr_s(<<"xmlns">>, Attrs), Name} of - {?NS_COMPRESS, <<"compress">>} when Zlib == true, - ((SockMod == gen_tcp) or - (SockMod == mongoose_tls)) -> - check_compression_auth(El, NextState, StateData); - _ -> - process_unauthenticated_stanza(StateData, El), - fsm_next_state(NextState, StateData) - - end. - -check_compression_auth(_El, NextState, StateData = #state{authenticated = false}) -> - send_element_from_server_jid(StateData, compress_setup_failed()), - fsm_next_state(NextState, StateData); -check_compression_auth(El, NextState, StateData) -> - check_compression_method(El, NextState, StateData). - -check_compression_method(El, NextState, StateData) -> - case exml_query:path(El, [{element, <<"method">>}, cdata]) of - undefined -> - send_element_from_server_jid(StateData, compress_setup_failed()), - fsm_next_state(NextState, StateData); - <<"zlib">> -> - {_, ZlibLimit} = StateData#state.zlib, - Socket = StateData#state.socket, - ZlibSocket - = (StateData#state.sockmod):compress(Socket, ZlibLimit, exml:to_binary(compressed())), - fsm_next_state(wait_for_stream, - StateData#state{socket = ZlibSocket, streamid = new_id()}); - _ -> - send_element_from_server_jid(StateData, compress_unsupported_method()), - fsm_next_state(NextState, StateData) - end. - --spec maybe_open_session(mongoose_acc:t(), state()) -> - {wait | stop | established, mongoose_acc:t(), state()}. -maybe_open_session(Acc, #state{jid = JID} = StateData) -> - case user_allowed(JID, StateData) of - true -> - do_open_session(Acc, JID, StateData); - _ -> - Acc1 = mongoose_hooks:forbidden_session_hook(StateData#state.host_type, - Acc, JID), - ?LOG_INFO(#{what => forbidden_session, - text => <<"User not allowed to open session">>, - acc => Acc, c2s_state => StateData}), - {Acc2, Err} = jlib:make_error_reply(Acc1, mongoose_xmpp_errors:not_allowed()), - Acc3 = send_element(Acc2, Err, StateData), - {wait, Acc3, StateData} - end. - --spec do_open_session(mongoose_acc:t(), jid:jid(), state()) -> - {stop | established, mongoose_acc:t(), state()}. -do_open_session(Acc, JID, StateData) -> - ?LOG_INFO(#{what => c2s_opened_session, text => <<"Opened session">>, - acc => Acc, c2s_state => StateData}), - Resp = jlib:make_result_iq_reply(mongoose_acc:element(Acc)), - Packet = {jid:to_bare(StateData#state.jid), StateData#state.jid, Resp}, - case send_and_maybe_buffer_stanza(Acc, Packet, StateData) of - {ok, Acc1, NStateData} -> - do_open_session_common(Acc1, JID, NStateData); - {resume, Acc1, NStateData} -> - case maybe_enter_resume_session(NStateData) of - {stop, normal, NextStateData} -> % error, resume not possible - c2s_stream_error(mongoose_xmpp_errors:stream_internal_server_error(), NextStateData), - {stop, Acc1, NStateData}; - {_, _, NextStateData, _} -> - do_open_session_common(Acc1, JID, NextStateData) - end - end. - -do_open_session_common(Acc, JID, #state{host_type = HostType, - jid = JID} = NewStateData0) -> - change_shaper(NewStateData0, JID), - Acc1 = mongoose_hooks:roster_get_subscription_lists(HostType, Acc, JID), - {Fs, Ts, Pending} = mongoose_acc:get(roster, subscription_lists, {[], [], []}, Acc1), - LJID = jid:to_lower(jid:to_bare(JID)), - Fs1 = [LJID | Fs], - Ts1 = [LJID | Ts], - PrivList = mongoose_hooks:privacy_get_user_list(HostType, JID), - SID = ejabberd_sm:make_new_sid(), - Conn = get_conn_type(NewStateData0), - Info = #{ip => NewStateData0#state.ip, conn => Conn, - auth_module => NewStateData0#state.auth_module }, - ReplacedPids = ejabberd_sm:open_session(HostType, SID, JID, Info), - - RefsAndPids = [{monitor(process, PID), PID} || PID <- ReplacedPids], - case RefsAndPids of - [] -> - ok; - _ -> - Timeout = get_replaced_wait_timeout(HostType), - erlang:send_after(Timeout, self(), replaced_wait_timeout) - end, - - NewStateData = - NewStateData0#state{sid = SID, - conn = Conn, - replaced_pids = RefsAndPids, - pres_f = gb_sets:from_list(Fs1), - pres_t = gb_sets:from_list(Ts1), - pending_invitations = Pending, - privacy_list = PrivList}, - {established, Acc1, NewStateData}. - -get_replaced_wait_timeout(HostType) -> - mongoose_config:get_opt({replaced_wait_timeout, HostType}). - --spec session_established(Item :: ejabberd:xml_stream_item(), - State :: state()) -> fsm_return(). -session_established({xmlstreamelement, - #xmlel{name = <<"enable">>} = El}, StateData) -> - maybe_enable_stream_mgmt(session_established, El, StateData); - -session_established({xmlstreamelement, - #xmlel{name = <<"a">>} = El}, StateData) -> - stream_mgmt_handle_ack(session_established, El, StateData); - -session_established({xmlstreamelement, - #xmlel{name = <<"r">>} = El}, StateData) -> - maybe_send_sm_ack(xml:get_tag_attr_s(<<"xmlns">>, El), - StateData#state.stream_mgmt, - StateData#state.stream_mgmt_in, - session_established, StateData); -session_established({xmlstreamelement, - #xmlel{name = <<"inactive">>} = El}, State) -> - mongoose_metrics:update(State#state.server, modCSIInactive, 1), - - maybe_inactivate_session(xml:get_tag_attr_s(<<"xmlns">>, El), State); - -session_established({xmlstreamelement, - #xmlel{name = <<"active">>} = El}, State) -> - mongoose_metrics:update(State#state.server, modCSIActive, 1), - - maybe_activate_session(xml:get_tag_attr_s(<<"xmlns">>, El), State); - -session_established({xmlstreamelement, El}, StateData) -> - FromJID = StateData#state.jid, - % Check 'from' attribute in stanza RFC 3920 Section 9.1.2 - case check_from(El, FromJID) of - 'invalid-from' -> - c2s_stream_error(mongoose_xmpp_errors:invalid_from(), StateData); - _NewEl -> - NewState = maybe_increment_sm_incoming(StateData#state.stream_mgmt, - StateData), - % initialise accumulator, fill with data - El1 = fix_message_from_user(El, StateData#state.lang), - Acc0 = element_to_origin_accum(El1, StateData), - Acc1 = mongoose_hooks:c2s_preprocessing_hook(StateData#state.host_type, - Acc0, NewState), - case mongoose_acc:get(hook, result, undefined, Acc1) of - drop -> fsm_next_state(session_established, NewState); - _ -> process_outgoing_stanza(Acc1, NewState) - end - end; - -%% We hibernate the process to reduce memory consumption after a -%% configurable activity timeout -session_established(timeout, StateData) -> - {next_state, session_established, StateData, hibernate()}; -session_established({xmlstreamend, _Name}, StateData) -> - send_trailer(StateData), - {stop, normal, StateData}; - -session_established({xmlstreamerror, <<"child element too big">> = E}, StateData) -> - PolicyViolationErr = mongoose_xmpp_errors:policy_violation(StateData#state.lang, E), - c2s_stream_error(PolicyViolationErr, StateData); -session_established({xmlstreamerror, _}, StateData) -> - c2s_stream_error(mongoose_xmpp_errors:xml_not_well_formed(), StateData); -session_established(closed, StateData) -> - ?LOG_DEBUG(#{what => c2s_closed, c2s_state => StateData, - text => <<"Session established closed - trying to enter resume_session">>}), - maybe_enter_resume_session(StateData#state.stream_mgmt_id, StateData). - -%% @doc Process packets sent by user (coming from user on c2s XMPP -%% connection) -%% eventually it should return {mongoose_acc:t(), fsm_return()} so that the accumulator -%% comes back whence it originated --spec process_outgoing_stanza(mongoose_acc:t(), state()) -> fsm_return(). -process_outgoing_stanza(Acc, StateData) -> - ToJID = mongoose_acc:to_jid(Acc), - Element = mongoose_acc:element(Acc), - NState = process_outgoing_stanza(Acc, ToJID, Element#xmlel.name, StateData), - fsm_next_state(session_established, NState). - -process_outgoing_stanza(Acc, ToJID, <<"presence">>, StateData) -> - FromJID = mongoose_acc:from_jid(Acc), - HostType = mongoose_acc:host_type(Acc), - Res = mongoose_hooks:c2s_update_presence(HostType, Acc), - El = mongoose_acc:element(Res), - Res1 = mongoose_hooks:user_send_packet(Res, FromJID, ToJID, El), - {_Acc1, NState} = case jid:are_bare_equal(FromJID, ToJID) of - true -> - presence_update(Res1, FromJID, StateData); - _ -> - presence_track(Res1, StateData) - end, - NState; -process_outgoing_stanza(Acc0, ToJID, <<"iq">>, StateData) -> - {XMLNS, Acc} = mongoose_iq:xmlns(Acc0), - FromJID = mongoose_acc:from_jid(Acc), - El = mongoose_acc:element(Acc), - {_Acc, NState} = case XMLNS of - ?NS_PRIVACY -> - process_privacy_iq(Acc, ToJID, StateData); - ?NS_BLOCKING -> - process_privacy_iq(Acc, ToJID, StateData); - _ -> - Acc2 = mongoose_hooks:user_send_packet(Acc, FromJID, - ToJID, El), - Acc3 = check_privacy_and_route(Acc2, StateData), - {Acc3, StateData} - end, - NState; -process_outgoing_stanza(Acc, ToJID, <<"message">>, StateData) -> - FromJID = mongoose_acc:from_jid(Acc), - El = mongoose_acc:element(Acc), - Acc1 = mongoose_hooks:user_send_packet(Acc, FromJID, ToJID, El), - _Acc2 = check_privacy_and_route(Acc1, StateData), - StateData; -process_outgoing_stanza(_Acc, _ToJID, _Name, StateData) -> - StateData. - -%%------------------------------------------------------------------------- -%% session may be terminated for example by mod_ping there is still valid -%% connection and resource want to send stanza. -resume_session({xmlstreamelement, _}, StateData) -> - Err = mongoose_xmpp_errors:policy_violation(StateData#state.lang, - <<"session in resume state cannot accept incoming stanzas">>), - maybe_send_element_from_server_jid_safe(StateData, Err), - maybe_send_trailer_safe(StateData), - {next_state, resume_session, StateData, hibernate()}; - -%%------------------------------------------------------------------------- -%% ignore mod_ping closed messages because we are already in resume session -%% state -resume_session(closed, StateData) -> - {next_state, resume_session, StateData, hibernate()}; -resume_session(timeout, StateData) -> - {next_state, resume_session, StateData, hibernate()}; -resume_session(Msg, StateData) -> - ?UNEXPECTED_INFO(Msg), - {next_state, resume_session, StateData, hibernate()}. - - -%%---------------------------------------------------------------------- -%% Func: StateName/3 -%% Returns: {next_state, NextStateName, NextStateData} | -%% {next_state, NextStateName, NextStateData, Timeout} | -%% {reply, Reply, NextStateName, NextStateData} | -%% {reply, Reply, NextStateName, NextStateData, Timeout} | -%% {stop, Reason, NewStateData} | -%% {stop, Reason, Reply, NewStateData} -%%---------------------------------------------------------------------- - -session_established(resume, From, SD) -> - handover_session(SD, From). - -resume_session(resume, From, SD) -> - handover_session(SD, From). - -%%---------------------------------------------------------------------- -%% Func: handle_event/3 -%% Returns: {next_state, NextStateName, NextStateData} | -%% {next_state, NextStateName, NextStateData, Timeout} | -%% {stop, Reason, NewStateData} -%%---------------------------------------------------------------------- - -handle_event(keep_alive_packet, session_established, - #state{host_type = HostType, jid = JID} = StateData) -> - mongoose_hooks:user_sent_keep_alive(HostType, JID), - fsm_next_state(session_established, StateData); -handle_event(_Event, StateName, StateData) -> - fsm_next_state(StateName, StateData). - -%%---------------------------------------------------------------------- -%% Func: handle_sync_event/4 -%% Returns: {next_state, NextStateName, NextStateData} | -%% {next_state, NextStateName, NextStateData, Timeout} | -%% {reply, Reply, NextStateName, NextStateData} | -%% {reply, Reply, NextStateName, NextStateData, Timeout} | -%% {stop, Reason, NewStateData} | -%% {stop, Reason, Reply, NewStateData} -%%---------------------------------------------------------------------- --spec handle_sync_event(Evt :: atom(), - From :: any(), - StateName :: statename(), - State :: state()) --> {'reply', Reply :: [any()], statename(), state()} - | {'reply', Reply :: 'ok' | {_, _, _, _}, statename(), state(), timeout()}. -handle_sync_event(get_presence, _From, StateName, StateData) -> - #jid{luser = User, lresource = Resource} = StateData#state.jid, - PresLast = StateData#state.pres_last, - Show = get_showtag(PresLast), - Status = get_statustag(PresLast), - Reply = {User, Resource, Show, Status}, - fsm_reply(Reply, StateName, StateData); -handle_sync_event(get_info, _From, StateName, StateData) -> - {reply, make_c2s_info(StateData), StateName, StateData}; -handle_sync_event(get_subscribed, _From, StateName, StateData) -> - Subscribed = gb_sets:to_list(StateData#state.pres_f), - {reply, Subscribed, StateName, StateData}; -handle_sync_event(_Event, _From, StateName, StateData) -> - Reply = ok, - fsm_reply(Reply, StateName, StateData). - - -code_change(_OldVsn, StateName, StateData, _Extra) -> - {ok, StateName, StateData}. - -%%---------------------------------------------------------------------- -%% Func: handle_info/3 -%% Returns: {next_state, NextStateName, NextStateData} | -%% {next_state, NextStateName, NextStateData, Timeout} | -%% {stop, Reason, NewStateData} -%%---------------------------------------------------------------------- - - -%%% system events -handle_info({exit, Reason}, _StateName, StateData = #state{lang = Lang}) -> - Acc = new_acc(StateData, #{location => ?LOCATION, element => undefined}), - send_element(Acc, mongoose_xmpp_errors:stream_conflict(Lang, Reason), StateData), - send_trailer(StateData), - {stop, normal, StateData}; -handle_info(replaced, _StateName, StateData) -> - Lang = StateData#state.lang, - StreamConflict = mongoose_xmpp_errors:stream_conflict(Lang, <<"Replaced by new connection">>), - maybe_send_element_from_server_jid_safe(StateData, StreamConflict), - maybe_send_trailer_safe(StateData), - {stop, normal, StateData#state{authenticated = replaced}}; -handle_info(new_offline_messages, session_established, - #state{pres_last = Presence, pres_invis = Invisible} = StateData) - when Presence =/= undefined orelse Invisible -> - Acc = new_acc(StateData, #{location => ?LOCATION, element => undefined}), - resend_offline_messages(Acc, StateData), - {next_state, session_established, StateData}; -handle_info({'DOWN', Monitor, _Type, _Object, _Info}, _StateName, StateData) - when Monitor == StateData#state.socket_monitor -> - maybe_enter_resume_session(StateData#state.stream_mgmt_id, StateData); -handle_info({'DOWN', Monitor, _Type, Object, Info}, StateName, - #state{ replaced_pids = ReplacedPids } = StateData) -> - case lists:keytake(Monitor, 1, ReplacedPids) of - {value, {Monitor, Object}, NReplacedPids} -> - NStateData = StateData#state{ replaced_pids = NReplacedPids }, - fsm_next_state(StateName, NStateData); - _ -> - ?LOG_WARNING(#{what => unexpected_c2s_down_info, - text => <<"C2S process got DOWN message from unknown process">>, - monitor_ref => Monitor, monitor_pid => Object, down_info => Info, - state_name => StateName, c2s_state => StateData}), - fsm_next_state(StateName, StateData) - end; -handle_info(replaced_wait_timeout, StateName, #state{ replaced_pids = [] } = StateData) -> - fsm_next_state(StateName, StateData); -handle_info(replaced_wait_timeout, StateName, #state{ replaced_pids = ReplacedPids } = StateData) -> - lists:foreach( - fun({Monitor, Pid}) -> - ?LOG_WARNING(#{what => c2s_replaced_wait_timeout, - text => <<"Some processes are not responding when handling replace messages">>, - monitor_ref => Monitor, replaced_pid => Pid, - state_name => StateName, c2s_state => StateData}) - end, ReplacedPids), - fsm_next_state(StateName, StateData#state{ replaced_pids = [] }); -handle_info(system_shutdown, StateName, StateData) -> - case StateName of - wait_for_stream -> - send_header(StateData, ?MYNAME, <<"1.0">>, <<"en">>), - ok; - _ -> - ok - end, - c2s_stream_error(mongoose_xmpp_errors:system_shutdown(), StateData); -handle_info({force_update_presence, LUser}, StateName, - #state{user = LUser, host_type = HostType} = StateData) -> - NewStateData = - case StateData#state.pres_last of - #xmlel{name = <<"presence">>} = PresEl -> - Acc = element_to_origin_accum(PresEl, StateData), - mongoose_hooks:c2s_update_presence(HostType, Acc), - {_Acc, StateData2} = presence_update(Acc, StateData#state.jid, StateData), - StateData2; - _ -> - StateData - end, - {next_state, StateName, NewStateData}; -handle_info(resume_timeout, resume_session, StateData) -> - {stop, normal, StateData}; -handle_info(check_buffer_full, StateName, StateData) -> - case is_buffer_full(StateData#state.stream_mgmt_buffer_size, - StateData#state.stream_mgmt_buffer_max) of - true -> - Err = mongoose_xmpp_errors:stream_resource_constraint((StateData#state.lang), - <<"too many unacked stanzas">>), - c2s_stream_error(Err, StateData); - false -> - fsm_next_state(StateName, - StateData#state{stream_mgmt_constraint_check_tref = undefined}) - end; -handle_info({store_session_info, JID, Key, Value, _FromPid}, StateName, StateData) -> - ejabberd_sm:store_info(JID, Key, Value), - fsm_next_state(StateName, StateData); -handle_info({remove_session_info, JID, Key, _FromPid}, StateName, StateData) -> - ejabberd_sm:remove_info(JID, Key), - fsm_next_state(StateName, StateData); -handle_info(Info, StateName, StateData) -> - handle_incoming_message(Info, StateName, StateData). - -%%% incoming messages from other users or services to this device -handle_incoming_message({send_text, Text}, StateName, StateData) -> - % it seems to be sometimes, by event sent from s2s - send_text(StateData, Text), - fsm_next_state(StateName, StateData); -handle_incoming_message({broadcast, Broadcast}, StateName, StateData) -> - Acc = new_acc(StateData, #{location => ?LOCATION, element => undefined}), - ?LOG_DEBUG(#{what => c2s_broadcast, - brodcast_data => Broadcast, - state_name => StateName, c2s_state => StateData}), - {Acc1, Res} = handle_routed_broadcast(Acc, Broadcast, StateData), - handle_broadcast_result(Acc1, Res, StateName, StateData); -handle_incoming_message({route, From, To, Acc}, StateName, StateData) -> - process_incoming_stanza_with_conflict_check(From, To, Acc, StateName, StateData); -handle_incoming_message({send_filtered, Feature, From, To, Packet}, StateName, StateData) -> - % this is used by pubsub and should be rewritten when someone rewrites pubsub module - Acc = new_acc(StateData, #{location => ?LOCATION, - from_jid => From, - to_jid => To, - element => Packet}), - Drop = mongoose_hooks:c2s_filter_packet(StateData, Feature, To, Packet), - case {Drop, StateData#state.jid} of - {true, _} -> - ?LOG_DEBUG(#{what => c2s_dropped_packet, acc => Acc, - text => <<"c2s_filter_packet hook dropped a packet">>}), - fsm_next_state(StateName, StateData); - {_, To} -> - FinalPacket = jlib:replace_from_to(From, To, Packet), - case p_privacy_check_packet(FinalPacket, From, To, in, StateData) of - allow -> - {Act, _, NStateData} = send_and_maybe_buffer_stanza(Acc, - {From, To, FinalPacket}, - StateData), - finish_state(Act, StateName, NStateData); - _ -> - fsm_next_state(StateName, StateData) - end; - _ -> - FinalPacket = jlib:replace_from_to(From, To, Packet), - ejabberd_router:route(From, To, FinalPacket), - fsm_next_state(StateName, StateData) - end; -handle_incoming_message({run_remote_hook, HandlerName, Args}, StateName, StateData) -> - HostType = ejabberd_c2s_state:host_type(StateData), - HandlerState = maps:get(HandlerName, StateData#state.handlers, empty_state), - case mongoose_hooks:c2s_remote_hook(HostType, HandlerName, Args, - HandlerState, StateData) of - {error, E} -> - ?LOG_ERROR(#{what => custom_c2s_hook_handler_error, - text => <<"c2s_remote_hook failed">>, reason => E, - handler_name => HandlerName, handler_args => Args, - handler_state => HandlerState, - state_name => StateName, c2s_state => StateData}), - fsm_next_state(StateName, StateData); - NewHandlerState -> - NewStates = maps:put(HandlerName, NewHandlerState, StateData#state.handlers), - fsm_next_state(StateName, StateData#state{handlers = NewStates}) - end; -handle_incoming_message(Info, StateName, StateData) -> - ?UNEXPECTED_INFO(Info), - fsm_next_state(StateName, StateData). - -process_incoming_stanza_with_conflict_check(From, To, Acc, StateName, StateData) -> - Check1 = check_incoming_accum_for_conflicts(Acc, StateData), - Check2 = check_receiver_sid_conflict(Acc, StateData), - case {Check1, Check2} of - {conflict, _} -> %% A race condition detected - %% Same jid, but different sids - OriginSID = mongoose_acc:get(c2s, origin_sid, undefined, Acc), - ?LOG_WARNING(#{what => conflict_check_failed, - text => <<"Drop Acc that is addressed to another connection " - "(origin SID check failed)">>, - c2s_sid => StateData#state.sid, origin_sid => OriginSID, - acc => Acc, state_name => StateName, c2s_state => StateData}), - finish_state(ok, StateName, StateData); - {_, conflict} -> - ReceiverSID = mongoose_acc:get(c2s, receiver_sid, undefined, Acc), - ?LOG_WARNING(#{what => conflict_check_failed, - text => <<"Drop Acc that is addressed to another connection " - "(receiver SID check failed)">>, - c2s_sid => StateData#state.sid, receiver_sid => ReceiverSID, - acc => Acc, state_name => StateName, c2s_state => StateData}), - finish_state(ok, StateName, StateData); - _ -> %% Continue processing - process_incoming_stanza(From, To, Acc, StateName, StateData) - end. - -check_receiver_sid_conflict(Acc, #state{sid = Sid}) -> - case mongoose_acc:get(c2s, receiver_sid, undefined, Acc) of - undefined -> - ok; - Sid -> - ok; - _ -> - conflict - end. - -%% If jid is the same, but sid is not, then we have a conflict. -%% jid example is alice@localhost/res1. -%% sid example is `{now(), pid()}'. -%% The conflict can happen, when actions with an accumulator were initiated by -%% one process but the resulting stanzas were routed to another process with -%% the same JID but different SID. -%% The conflict usually happens when an user is reconnecting. -%% Both origin_sid and origin_jid props should be defined. -%% But we don't force developers to set both of them, so we should correctly -%% process stanzas, that have only one properly set. -%% -%% "Incoming" means that stanza is coming from ejabberd_router. --spec check_incoming_accum_for_conflicts(mongoose_acc:t(), state()) -> - unknown_origin | different_origin | same_device | conflict. -check_incoming_accum_for_conflicts(Acc, #state{sid = SID, jid = JID, - stream_mgmt_resumed_from = OldSID}) -> - OriginSID = mongoose_acc:get(c2s, origin_sid, undefined, Acc), - OriginJID = mongoose_acc:get(c2s, origin_jid, undefined, Acc), - AreDefined = OriginJID =/= undefined andalso OriginSID =/= undefined, - case AreDefined of - false -> - unknown_origin; - true -> - SameJID = jid:are_equal(OriginJID, JID), - SameSID = OriginSID =:= SID, - % It's possible to receive a response addressed to a process - % which we resumed from - still valid! - SameOldSession = OriginSID =:= OldSID, - case {SameJID, SameSID or SameOldSession} of - {false, _} -> - different_origin; - {_, true} -> - same_device; - _ -> - conflict - end - end. - -process_incoming_stanza(From, To, Acc, StateName, StateData) -> - #xmlel{ name = Name } = Packet = mongoose_acc:element(Acc), - {Act, _NextAcc, NextState} = case handle_routed(Name, From, To, Acc, StateData) of - {allow, NewAcc, NewPacket, NewState} -> - preprocess_and_ship(NewAcc, From, To, NewPacket, NewState); - {allow, NewAcc, NewState} -> - preprocess_and_ship(NewAcc, From, To, Packet, NewState); - {Reason, NewAcc, NewState} -> - response_negative(Name, Reason, From, To, NewAcc), - {ok, NewAcc, NewState} - end, - finish_state(Act, StateName, NextState). - - --spec preprocess_and_ship(Acc :: mongoose_acc:t(), - From :: jid:jid(), - To :: jid:jid(), - El :: exml:element(), - StateData :: state()) -> {ok | resume, mongoose_acc:t(), state()}. -preprocess_and_ship(Acc, From, To, El, StateData) -> - #xmlel{attrs = Attrs} = El, - Attrs2 = jlib:replace_from_to_attrs(jid:to_binary(From), - jid:to_binary(To), - Attrs), - FixedEl = El#xmlel{attrs = Attrs2}, - Acc2 = mongoose_hooks:user_receive_packet(StateData#state.host_type, Acc, - StateData#state.jid, From, To, FixedEl), - ship_to_local_user(Acc2, {From, To, FixedEl}, StateData). - -response_negative(<<"iq">>, forbidden, From, To, Acc) -> - send_back_error(mongoose_xmpp_errors:forbidden(), From, To, Acc); -response_negative(<<"iq">>, deny, From, To, Acc) -> - IqType = mongoose_acc:stanza_type(Acc), - response_iq_deny(IqType, From, To, Acc); -response_negative(<<"message">>, deny, From, To, Acc) -> - Acc1 = mod_amp:check_packet(Acc, delivery_failed), - send_back_error(mongoose_xmpp_errors:service_unavailable(), From, To, Acc1); -response_negative(_, _, _, _, Acc) -> - Acc. - -response_iq_deny(<<"get">>, From, To, Acc) -> - send_back_error(mongoose_xmpp_errors:service_unavailable(), From, To, Acc); -response_iq_deny(<<"set">>, From, To, Acc) -> - send_back_error(mongoose_xmpp_errors:service_unavailable(), From, To, Acc); -response_iq_deny(_, _, _, Acc) -> - Acc. - -send_back_error(Etype, From, To, Acc) -> - {Acc1, Err} = jlib:make_error_reply(Acc, Etype), - ejabberd_router:route(To, From, Acc1, Err). - -handle_routed(<<"presence">>, From, To, Acc, StateData) -> - handle_routed_presence(From, To, Acc, StateData); -handle_routed(<<"iq">>, From, To, Acc, StateData) -> - handle_routed_iq(From, To, Acc, StateData); -handle_routed(<<"message">>, _From, To, Acc, StateData) -> - {Res, Acc1} = s_privacy_check_packet(Acc, To, in, StateData), - case Res of - allow -> - {allow, Acc1, StateData}; - deny -> - {deny, Acc1, StateData}; - block -> - {deny, Acc1, StateData} - end; -handle_routed(_, _From, _To, Acc, StateData) -> - {ignore, Acc, StateData}. - --spec handle_routed_iq(From :: jid:jid(), - To :: jid:jid(), - Acc :: mongoose_acc:t(), - StateData :: state()) -> routing_result(). -handle_routed_iq(From, To, Acc, StateData) -> - {Qi, Acc1} = mongoose_iq:info(Acc), - handle_routed_iq(From, To, Acc1, Qi, StateData). - --spec handle_routed_iq(From :: jid:jid(), - To :: jid:jid(), - Acc :: mongoose_acc:t(), - IQ :: invalid | not_iq | jlib:iq(), - StateData :: state()) -> routing_result(). -handle_routed_iq(From, To, Acc0, #iq{ xmlns = ?NS_LAST, type = Type }, StateData) - when Type /= result, Type /= error -> - % we could make iq handlers handle full jids as well, but wouldn't it be an overkill? - {Acc, HasFromSub} = case is_subscribed_to_my_presence(From, StateData) of - true -> - {R, A} = s_privacy_check_packet(Acc0, To, out, StateData), - {A, R == 'allow'}; - false -> - {Acc0, false} - end, - case HasFromSub of - true -> - {Res, Acc1} = s_privacy_check_packet(Acc, To, in, StateData), - case Res of - allow -> - {allow, Acc1, StateData}; - _ -> - {deny, Acc1, StateData} - end; - _ -> - {forbidden, Acc, StateData} - end; -handle_routed_iq(_From, To, Acc, #iq{}, StateData) -> - {Res, Acc1} = s_privacy_check_packet(Acc, To, in, StateData), - case Res of - allow -> - {allow, Acc1, StateData}; - deny -> - {deny, Acc1, StateData} - end; -handle_routed_iq(_From, _To, Acc, IQ, StateData) - when (IQ == invalid) or (IQ == not_iq) -> - {invalid, Acc, StateData}. - --spec handle_routed_broadcast(Acc :: mongoose_acc:t(), - Broadcast :: broadcast_type(), - StateData :: state()) -> - {mongoose_acc:t(), broadcast_result()}. -handle_routed_broadcast(Acc, {item, IJID, ISubscription}, StateData) -> - {Acc2, NewState} = roster_change(Acc, IJID, ISubscription, StateData), - {Acc2, {new_state, NewState}}; -handle_routed_broadcast(Acc, {privacy_list, PrivList, PrivListName}, StateData) -> - case mongoose_hooks:privacy_updated_list(StateData#state.host_type, - StateData#state.privacy_list, PrivList) of - false -> - {Acc, {new_state, StateData}}; - NewPL -> - PrivPushIQ = privacy_list_push_iq(PrivListName), - F = jid:to_bare(StateData#state.jid), - T = StateData#state.jid, - PrivPushEl = jlib:replace_from_to(F, T, jlib:iq_to_xml(PrivPushIQ)), - Acc1 = maybe_update_presence(Acc, StateData, NewPL), - Res = {send_new, F, T, PrivPushEl, StateData#state{privacy_list = NewPL}}, - {Acc1, Res} - end; -handle_routed_broadcast(Acc, {blocking, UserList, Action, JIDs}, StateData) -> - blocking_push_to_resources(Action, JIDs, StateData), - blocking_presence_to_contacts(Action, JIDs, StateData), - Res = {new_state, StateData#state{privacy_list = UserList}}, - {Acc, Res}; -handle_routed_broadcast(Acc, _, StateData) -> - {Acc, {new_state, StateData}}. - --spec handle_broadcast_result(mongoose_acc:t(), broadcast_result(), StateName :: atom(), - StateData :: state()) -> - any(). -handle_broadcast_result(Acc, {send_new, From, To, Stanza, NewState}, StateName, _StateData) -> - {Act, _, NewStateData} = ship_to_local_user(Acc, {From, To, Stanza}, NewState), - finish_state(Act, StateName, NewStateData); -handle_broadcast_result(_Acc, {new_state, NewState}, StateName, _StateData) -> - fsm_next_state(StateName, NewState). - -privacy_list_push_iq(PrivListName) -> - #iq{type = set, xmlns = ?NS_PRIVACY, - id = <<"push", (mongoose_bin:gen_from_crypto())/binary>>, - sub_el = [#xmlel{name = <<"query">>, - attrs = [{<<"xmlns">>, ?NS_PRIVACY}], - children = [#xmlel{name = <<"list">>, - attrs = [{<<"name">>, PrivListName}]}]}]}. - --spec handle_routed_presence(From :: jid:jid(), To :: jid:jid(), - Acc0 :: mongoose_acc:t(), StateData :: state()) -> routing_result(). -handle_routed_presence(From, To, Acc, StateData) -> - Packet = mongoose_acc:element(Acc), - State = mongoose_hooks:c2s_presence_in(StateData, From, To, Packet), - case mongoose_acc:stanza_type(Acc) of - <<"probe">> -> - {LFrom, LBFrom} = lowcase_and_bare(From), - NewState = case am_i_available_to(LFrom, LBFrom, State) of - true -> State; - false -> make_available_to(LFrom, LBFrom, State) - end, - Acc1 = process_presence_probe(From, To, Acc, NewState), - {probe, Acc1, NewState}; - <<"error">> -> - NewA = gb_sets:del_element(jid:to_lower(From), State#state.pres_a), - {allow, Acc, State#state{pres_a = NewA}}; - <<"invisible">> -> - #xmlel{ attrs = Attrs } = Packet, - Attrs1 = lists:keydelete(<<"type">>, 1, Attrs), - Attrs2 = [{<<"type">>, <<"unavailable">>} | Attrs1], - NEl = Packet#xmlel{attrs = Attrs2}, - {allow, Acc, NEl, State}; - <<"subscribe">> -> - {SRes, Acc1} = s_privacy_check_packet(Acc, To, in, State), - {SRes, Acc1, State}; - <<"subscribed">> -> - {SRes, Acc1} = s_privacy_check_packet(Acc, To, in, State), - {SRes, Acc1, State}; - <<"unsubscribe">> -> - {SRes, Acc1} = s_privacy_check_packet(Acc, To, in, State), - {SRes, Acc1, State}; - <<"unsubscribed">> -> - {SRes, Acc1} = s_privacy_check_packet(Acc, To, in, State), - {SRes, Acc1, State}; - _ -> - handle_routed_available_presence(State, From, To, Acc) - end. - --spec handle_routed_available_presence(State :: state(), - From :: jid:jid(), - To :: jid:jid(), - Acc :: mongoose_acc:t()) -> routing_result(). -handle_routed_available_presence(State, From, To, Acc) -> - {Res, Acc1} = s_privacy_check_packet(Acc, To, in, State), - case Res of - allow -> - {LFrom, LBFrom} = lowcase_and_bare(From), - case am_i_available_to(LFrom, LBFrom, State) of - true -> {allow, Acc1, State}; - false -> {allow, Acc1, make_available_to(LFrom, LBFrom, State)} - end; - _ -> - {deny, Acc1, State} - end. - -am_i_available_to(LFrom, LBFrom, State) -> - gb_sets:is_element(LFrom, State#state.pres_a) - orelse (LFrom /= LBFrom) - andalso gb_sets:is_element(LBFrom, State#state.pres_a). - -make_available_to(LFrom, LBFrom, State) -> - case gb_sets:is_element(LFrom, State#state.pres_f) of - true -> - A = gb_sets:add_element(LFrom, State#state.pres_a), - State#state{pres_a = A}; - false -> - case gb_sets:is_element(LBFrom, State#state.pres_f) of - true -> - A = gb_sets:add_element(LBFrom, State#state.pres_a), - State#state{pres_a = A}; - false -> - State - end - end. - -%%---------------------------------------------------------------------- -%% Func: print_state/1 -%% Purpose: Prepare the state to be printed on error log -%% Returns: State to print -%%---------------------------------------------------------------------- --spec print_state(state()) -> state(). -print_state(State = #state{pres_t = T, pres_f = F, pres_a = A, pres_i = I}) -> - State#state{pres_t = {pres_t, gb_sets:size(T)}, - pres_f = {pres_f, gb_sets:size(F)}, - pres_a = {pres_a, gb_sets:size(A)}, - pres_i = {pres_i, gb_sets:size(I)} - }. - -%%---------------------------------------------------------------------- -%% Func: terminate/4 -%% Purpose: Shutdown the fsm -%% Returns: any -%%---------------------------------------------------------------------- --spec terminate(Reason :: any(), statename(), state(), list()) -> ok. -terminate({handover_session, From}, StateName, StateData, UnreadMessages) -> - % do handover first - NewStateData = do_handover_session(StateData, UnreadMessages), - p1_fsm_old:reply(From, {ok, NewStateData}), - % and then run the normal termination - terminate(normal, StateName, NewStateData, []); -terminate(_Reason, StateName, StateData, UnreadMessages) -> - InitialAcc0 = new_acc(StateData, #{location => ?LOCATION, element => undefined}), - Acc = case StateData#state.stream_mgmt of - true -> mongoose_acc:set(stream_mgmt, h, StateData#state.stream_mgmt_in, InitialAcc0); - _ -> InitialAcc0 - end, - case {should_close_session(StateName), StateData#state.authenticated} of - {false, _} -> - ok; - %% if we are in an state which has a session established - {_, replaced} -> - ?LOG_INFO(#{what => replaced_session, - text => <<"Replaced by new connection">>, - state_name => StateName, c2s_state => StateData}), - StatusEl = #xmlel{name = <<"status">>, - children = [#xmlcdata{content = <<"Replaced by new connection">>}]}, - Packet = #xmlel{name = <<"presence">>, - attrs = [{<<"type">>, <<"unavailable">>}], - children = [StatusEl]}, - Acc0 = element_to_origin_accum(Packet, StateData), - ejabberd_sm:close_session_unset_presence( - Acc, - StateData#state.sid, - StateData#state.jid, - <<"Replaced by new connection">>, - replaced), - Acc1 = presence_broadcast(Acc0, StateData#state.pres_a, StateData), - presence_broadcast(Acc1, StateData#state.pres_i, StateData), - reroute_unacked_messages(StateData, UnreadMessages); - {_, resumed} -> - StreamConflict = mongoose_xmpp_errors:stream_conflict( - StateData#state.lang, <<"Resumed by new connection">>), - maybe_send_element_from_server_jid_safe(StateData, StreamConflict), - maybe_send_trailer_safe(StateData), - ?LOG_INFO(#{what => stream_resumed, - stream_mgmt_in => StateData#state.stream_mgmt_id, - state_name => StateName, c2s_state => StateData}); - _ -> - ?LOG_INFO(#{what => close_session, - state_name => StateName, c2s_state => StateData}), - - EmptySet = gb_sets:new(), - case StateData of - #state{pres_last = undefined, - pres_a = EmptySet, - pres_i = EmptySet, - pres_invis = false} -> - ejabberd_sm:close_session(Acc, - StateData#state.sid, - StateData#state.jid, - normal); - _ -> - Packet = #xmlel{name = <<"presence">>, - attrs = [{<<"type">>, <<"unavailable">>}]}, - Acc0 = element_to_origin_accum(Packet, StateData), - ejabberd_sm:close_session_unset_presence( - Acc, - StateData#state.sid, - StateData#state.jid, - <<"">>, - normal), - Acc1 = presence_broadcast(Acc0, StateData#state.pres_a, StateData), - presence_broadcast(Acc1, StateData#state.pres_i, StateData) - end, - reroute_unacked_messages(StateData, UnreadMessages) - end, - (StateData#state.sockmod):close(StateData#state.socket), - ok. - --spec reroute_unacked_messages(StateData :: state(), list()) -> any(). -reroute_unacked_messages(StateData, UnreadMessages) -> - ?LOG_DEBUG(#{what => rerouting_unacked_messages, - unread_messages => UnreadMessages, c2s_state => StateData}), - flush_stream_mgmt_buffer(StateData), - bounce_csi_buffer(StateData), - bounce_messages(UnreadMessages, StateData). - -%%%---------------------------------------------------------------------- -%%% Internal functions -%%%---------------------------------------------------------------------- - -fix_message_from_user(#xmlel{attrs = Attrs} = El0, Lang) -> - NewEl1 = jlib:remove_delay_tags(El0), - case xml:get_attr_s(<<"xml:lang">>, Attrs) of - <<>> -> - case Lang of - <<>> -> NewEl1; - Lang -> - xml:replace_tag_attr(<<"xml:lang">>, Lang, NewEl1) - end; - _ -> - NewEl1 - end. - -should_close_session(resume_session) -> true; -should_close_session(session_established) -> true; -should_close_session(_) -> false. - --spec generate_random_resource() -> jid:lresource(). -generate_random_resource() -> - <<(mongoose_bin:gen_from_crypto())/binary, (mongoose_bin:gen_from_timestamp())/binary>>. - --spec change_shaper(state(), jid:jid()) -> any(). -change_shaper(#state{host_type = HostType, server = Server, shaper = ShaperRule, - socket = Socket, sockmod = SockMod}, JID) -> - Shaper = acl:match_rule(HostType, Server, ShaperRule, JID), - SockMod:change_shaper(Socket, Shaper). - --spec send_text(state(), Text :: binary()) -> any(). -send_text(StateData, Text) -> - ?LOG_DEBUG(#{what => c2s_send_text, text => <<"Send XML to the socket">>, - send_text => Text, c2s_state => StateData}), - Size = size(Text), - mongoose_metrics:update(global, [data, xmpp, sent, xml_stanza_size], Size), - (StateData#state.sockmod):send(StateData#state.socket, Text). - --spec maybe_send_element_from_server_jid_safe(state(), exml:element()) -> any(). -maybe_send_element_from_server_jid_safe(State, El) -> - maybe_send_element_from_server_jid_safe(no_acc, State, El). - --spec maybe_send_element_from_server_jid_safe(mongoose_acc:t() | no_acc, - state(), - exml:element()) -> any(). -maybe_send_element_from_server_jid_safe(Acc, #state{stream_mgmt = StreamMgmt} = State, El) - when StreamMgmt =:= false; StreamMgmt =:= disabled -> - send_element_from_server_jid(Acc, State, El); -maybe_send_element_from_server_jid_safe(Acc, State, El) -> - case catch send_element_from_server_jid(Acc, State, El) of - ok -> ok; - _ -> error - end. - --spec send_element_from_server_jid(state(), exml:element()) -> any(). -send_element_from_server_jid(StateData, #xmlel{} = El) -> - send_element_from_server_jid(no_acc, StateData, El). - --spec send_element_from_server_jid(mongoose_acc:t() | no_acc, state(), exml:element()) -> any(). -send_element_from_server_jid(no_acc, #state{server = Server} = StateData, #xmlel{} = El) -> - Acc = new_acc(StateData, #{location => ?LOCATION, - from_jid => jid:make_noprep(<<>>, Server, <<>>), - to_jid => StateData#state.jid, - element => El}), - send_element_from_server_jid(Acc, StateData, El); -send_element_from_server_jid(Acc, StateData, #xmlel{} = El) -> - Acc1 = send_element(Acc, El, StateData), - mongoose_acc:get(c2s, send_result, Acc1). - -%% @doc This is the termination point - from here stanza is sent to the user --spec send_element(mongoose_acc:t(), exml:element(), state()) -> mongoose_acc:t(). -send_element(Acc, El, #state{host_type = undefined} = StateData) -> - Res = do_send_element(El, StateData), - mongoose_acc:set(c2s, send_result, Res, Acc); -send_element(Acc, El, #state{host_type = HostType} = StateData) -> - Acc1 = mongoose_hooks:xmpp_send_element(HostType, Acc, El), - Res = do_send_element(El, StateData), - mongoose_acc:set(c2s, send_result, Res, Acc1). - -do_send_element(El, #state{sockmod = SockMod} = StateData) - when StateData#state.xml_socket -> - mongoose_transport:send_xml(SockMod, StateData#state.socket, - {xmlstreamelement, El}); -do_send_element(El, StateData) -> - send_text(StateData, exml:to_binary(El)). - --spec send_header(State :: state(), - Server :: jid:server(), - Version :: binary(), - Lang :: ejabberd:lang()) -> any(). -send_header(StateData, Server, Version, Lang) - when StateData#state.xml_socket -> - VersionAttr = case Version of - <<>> -> []; - _ -> [{<<"version">>, Version}] - end, - LangAttr = case Lang of - <<>> -> []; - _ -> [{<<"xml:lang">>, Lang}] - end, - Header = {xmlstreamstart, - <<"stream:stream">>, - VersionAttr ++ - LangAttr ++ - [{<<"xmlns">>, ?NS_CLIENT}, - {<<"xmlns:stream">>, <<"http://etherx.jabber.org/streams">>}, - {<<"id">>, StateData#state.streamid}, - {<<"from">>, Server}]}, - (StateData#state.sockmod):send_xml(StateData#state.socket, Header); -send_header(StateData, Server, Version, Lang) -> - VersionStr = case Version of - <<>> -> <<>>; - _ -> <<" version='", (Version)/binary, "'">> - end, - LangStr = case Lang of - <<>> -> <<>>; - _ when is_binary(Lang) -> <<" xml:lang='", (Lang)/binary, "'">> - end, - Header = <<"", - "">>, - send_text(StateData, Header). - --spec maybe_send_trailer_safe(State :: state()) -> any(). -maybe_send_trailer_safe(#state{stream_mgmt = StreamMgmt} = State) - when StreamMgmt =:= false; StreamMgmt =:= disabled -> - send_trailer(State); -maybe_send_trailer_safe(StateData) -> - catch send_trailer(StateData). - -send_trailer(StateData) when StateData#state.xml_socket -> - (StateData#state.sockmod):send_xml(StateData#state.socket, - {xmlstreamend, <<"stream:stream">>}); -send_trailer(StateData) -> - send_text(StateData, ?STREAM_TRAILER). - - --spec send_and_maybe_buffer_stanza(mongoose_acc:t(), packet(), state()) -> - {ok | resume, mongoose_acc:t(), state()}. -send_and_maybe_buffer_stanza(Acc, {J1, J2, El}, State)-> - {SendResult, _, BufferedStateData} = send_and_maybe_buffer_stanza_no_ack(Acc, - {J1, J2, El}, - State), - Acc1 = mod_amp:check_packet(Acc, result_to_amp_event(SendResult)), - case SendResult of - ok -> - try maybe_send_ack_request(Acc1, BufferedStateData) of - ResAcc -> - {ok, ResAcc, BufferedStateData} - catch - _:E -> - ?LOG_DEBUG(#{what => send_ack_request_error, - text => <<"maybe_send_ack_request crashed, entering resume session next">>, - reason => E, c2s_state => State}), - {resume, Acc1, BufferedStateData} - end; - _ -> - ?LOG_DEBUG(#{what => send_element_error, - text => <<"Sending element failed, entering resume session next">>, - reason => SendResult, c2s_state => State}), - {resume, Acc1, BufferedStateData} - end. - -result_to_amp_event(ok) -> delivered; -result_to_amp_event(_) -> delivery_failed. - --spec send_and_maybe_buffer_stanza_no_ack(mongoose_acc:t(), packet(), state()) -> - {ok | any(), mongoose_acc:t(), state()}. -send_and_maybe_buffer_stanza_no_ack(Acc, {_, _, Stanza} = Packet, State) -> - SendResult = maybe_send_element_from_server_jid_safe(Acc, State, Stanza), - BufferedStateData = buffer_out_stanza(Acc, Packet, State), - {SendResult, Acc, BufferedStateData}. - - --spec new_id() -> binary(). -new_id() -> - mongoose_bin:gen_from_crypto(). - --spec get_conn_type(state()) -> conntype(). -get_conn_type(StateData) -> - case (StateData#state.sockmod):get_sockmod(StateData#state.socket) of - gen_tcp -> c2s; - mongoose_tls -> c2s_tls; - ejabberd_zlib -> - case ejabberd_zlib:get_sockmod(ejabberd_socket:get_socket(StateData#state.socket)) of - gen_tcp -> c2s_compressed; - mongoose_tls -> c2s_compressed_tls - end; - _ -> unknown - end. - - --spec process_presence_probe(From :: jid:simple_jid() | jid:jid(), - To :: jid:jid(), - Acc :: mongoose_acc:t(), - State :: state()) -> mongoose_acc:t(). -process_presence_probe(From, To, Acc, StateData) -> - {LFrom, LBareFrom} = lowcase_and_bare(From), - case StateData#state.pres_last of - undefined -> - Acc; - _ -> - case {should_retransmit_last_presence(LFrom, LBareFrom, StateData), - specifically_visible_to(LFrom, StateData)} of - {true, _} -> - Timestamp = StateData#state.pres_timestamp, - TS = calendar:system_time_to_rfc3339(Timestamp, [{offset, "Z"}]), - Packet = xml:append_subtags( - StateData#state.pres_last, - %% To is the one sending the presence (the target of the probe) - [jlib:timestamp_to_xml(TS, To, <<>>)]), - check_privacy_and_route_probe(StateData, From, To, Acc, Packet); - {false, true} -> - ejabberd_router:route(To, From, Acc, #xmlel{name = <<"presence">>}); - _ -> - Acc - end - end. - --spec check_privacy_and_route_probe(StateData :: state(), - From :: jid:jid(), - To :: jid:jid(), - Acc :: mongoose_acc:t(), - Packet :: exml:element()) -> mongoose_acc:t(). -check_privacy_and_route_probe(StateData, From, To, Acc, Packet) -> - {Res, Acc1} = d_privacy_check_packet(Acc, Packet, To, From, out, StateData), - case Res of - allow -> - Pid = element(2, StateData#state.sid), - Acc2 = mongoose_hooks:presence_probe_hook( - StateData#state.host_type, - Acc1, - From, To, Pid), - %% Don't route a presence probe to oneself - case jid:are_equal(From, To) of - false -> - ejabberd_router:route(To, From, Acc2, Packet); - true -> - Acc2 - end; - _ -> - Acc1 - end. - -should_retransmit_last_presence(LFrom, LBareFrom, - #state{pres_invis = Invisible} = S) -> - not Invisible - andalso is_subscribed_to_my_presence(LFrom, LBareFrom, S) - andalso not invisible_to(LFrom, LBareFrom, S). - -is_subscribed_to_my_presence(JID, S) -> - {Lowcase, Bare} = lowcase_and_bare(JID), - is_subscribed_to_my_presence(Lowcase, Bare, S). - -is_subscribed_to_my_presence(LFrom, LBareFrom, S) -> - gb_sets:is_element(LFrom, S#state.pres_f) - orelse (LFrom /= LBareFrom) - andalso gb_sets:is_element(LBareFrom, S#state.pres_f). - -am_i_subscribed_to_presence(LJID, LBareJID, S) -> - gb_sets:is_element(LJID, S#state.pres_t) - orelse (LJID /= LBareJID) - andalso gb_sets:is_element(LBareJID, S#state.pres_t). - -lowcase_and_bare(JID) -> - LJID = jid:to_lower(JID), - { LJID, jid:to_bare(LJID)}. - -invisible_to(LFrom, LBareFrom, S) -> - gb_sets:is_element(LFrom, S#state.pres_i) - orelse (LFrom /= LBareFrom) - andalso gb_sets:is_element(LBareFrom, S#state.pres_i). - -%% @doc Is generally invisible, but visible to a particular resource? -specifically_visible_to(LFrom, #state{pres_invis = Invisible} = S) -> - Invisible - andalso gb_sets:is_element(LFrom, S#state.pres_f) - andalso gb_sets:is_element(LFrom, S#state.pres_a). - -%% @doc User updates his presence (non-directed presence packet) --spec presence_update(Acc :: mongoose_acc:t(), - From :: 'undefined' | jid:jid(), - State :: state()) -> {mongoose_acc:t(), state()}. -presence_update(Acc, From, StateData) -> - Packet = mongoose_acc:element(Acc), - case mongoose_acc:stanza_type(Acc) of - <<"unavailable">> -> - Status = exml_query:path(Packet, [{element, <<"status">>}, cdata], <<>>), - Info = #{ip => StateData#state.ip, conn => StateData#state.conn, - auth_module => StateData#state.auth_module }, - Acc1 = ejabberd_sm:unset_presence(Acc, - StateData#state.sid, - StateData#state.jid, - Status, - Info), - Acc2 = presence_broadcast(Acc1, StateData#state.pres_a, StateData), - Acc3 = presence_broadcast(Acc2, StateData#state.pres_i, StateData), - % and here we reach the end - {Acc3, StateData#state{pres_last = undefined, - pres_timestamp = undefined, - pres_a = gb_sets:new(), - pres_i = gb_sets:new(), - pres_invis = false}}; - <<"invisible">> -> - NewPriority = get_priority_from_presence(Packet), - Acc0 = update_priority(Acc, NewPriority, Packet, StateData), - case StateData#state.pres_invis of - false -> - Acc1 = presence_broadcast(Acc0, - StateData#state.pres_a, - StateData), - Acc2 = presence_broadcast(Acc1, - StateData#state.pres_i, - StateData), - S1 = StateData#state{pres_last = undefined, - pres_timestamp = undefined, - pres_a = gb_sets:new(), - pres_i = gb_sets:new(), - pres_invis = true}, - presence_broadcast_first(Acc2, From, S1, Packet); - true -> - {Acc0, StateData} - end; - <<"error">> -> - {Acc, StateData}; - <<"probe">> -> - {Acc, StateData}; - <<"subscribe">> -> - {Acc, StateData}; - <<"subscribed">> -> - {Acc, StateData}; - <<"unsubscribe">> -> - {Acc, StateData}; - <<"unsubscribed">> -> - {Acc, StateData}; - _ -> - presence_update_to_available(Acc, From, Packet, StateData) - end. - --spec presence_update_to_available(Acc :: mongoose_acc:t(), - From :: jid:jid(), - Packet :: exml:element(), - StateData :: state()) -> {mongoose_acc:t(), state()}. -presence_update_to_available(Acc, From, Packet, StateData) -> - OldPriority = case StateData#state.pres_last of - undefined -> - 0; - OldPresence -> - get_priority_from_presence(OldPresence) - end, - NewPriority = get_priority_from_presence(Packet), - Timestamp = erlang:system_time(second), - Acc1 = update_priority(Acc, NewPriority, Packet, StateData), - - NewStateData = StateData#state{pres_last = Packet, - pres_invis = false, - pres_timestamp = Timestamp}, - - FromUnavail = (StateData#state.pres_last == undefined) or StateData#state.pres_invis, - ?LOG_DEBUG(#{what => presence_update_to_available, - text => <<"Presence changes from unavailable to available">>, - from_unavail => FromUnavail, acc => Acc, c2s_state => StateData}), - presence_update_to_available(FromUnavail, Acc1, OldPriority, NewPriority, From, - Packet, NewStateData). - -%% @doc the first one is run when presence changes from unavailable to anything else --spec presence_update_to_available(FromUnavailable :: boolean(), - Acc :: mongoose_acc:t(), - OldPriority :: integer(), - NewPriority :: integer(), - From :: jid:jid(), - Packet :: exml:element(), - StateData :: state()) -> {mongoose_acc:t(), state()}. -presence_update_to_available(true, Acc, _, NewPriority, From, Packet, StateData) -> - Acc2 = mongoose_hooks:user_available_hook(Acc, StateData#state.jid), - Res = case NewPriority >= 0 of - true -> - Acc3 = mongoose_hooks:roster_get_subscription_lists( - StateData#state.host_type, - Acc2, - StateData#state.jid), - {_, _, Pending} = mongoose_acc:get(roster, subscription_lists, - {[], [], []}, Acc3), - Acc4 = resend_offline_messages(Acc3, StateData), - resend_subscription_requests(Acc4, - StateData#state{pending_invitations = Pending}); - false -> - {Acc2, StateData} - end, - {Accum, NewStateData1} = Res, - presence_broadcast_first(Accum, From, NewStateData1, Packet); -presence_update_to_available(false, Acc, OldPriority, NewPriority, From, Packet, StateData) -> - Acc2 = presence_broadcast_to_trusted(Acc, - StateData, - From, - StateData#state.pres_f, - StateData#state.pres_a, - Packet), - Acc3 = case OldPriority < 0 andalso NewPriority >= 0 of - true -> - resend_offline_messages(Acc2, StateData); - false -> - Acc2 - end, - {Acc3, StateData}. - -%% @doc User sends a directed presence packet --spec presence_track(Acc :: mongoose_acc:t(), - State :: state()) -> {mongoose_acc:t(), state()}. -presence_track(Acc, StateData) -> - To = mongoose_acc:to_jid(Acc), - LTo = jid:to_lower(To), - case mongoose_acc:stanza_type(Acc) of - <<"unavailable">> -> - Acc1 = check_privacy_and_route(Acc, StateData), - I = gb_sets:del_element(LTo, StateData#state.pres_i), - A = gb_sets:del_element(LTo, StateData#state.pres_a), - {Acc1, StateData#state{pres_i = I, - pres_a = A}}; - <<"invisible">> -> - Acc1 = check_privacy_and_route(Acc, StateData), - I = gb_sets:add_element(LTo, StateData#state.pres_i), - A = gb_sets:del_element(LTo, StateData#state.pres_a), - {Acc1, StateData#state{pres_i = I, - pres_a = A}}; - <<"subscribe">> -> - Acc1 = process_presence_subscription_and_route(Acc, subscribe, StateData), - {Acc1, StateData}; - <<"subscribed">> -> - Acc1 = process_presence_subscription_and_route(Acc, subscribed, StateData), - {Acc1, StateData}; - <<"unsubscribe">> -> - Acc1 = process_presence_subscription_and_route(Acc, unsubscribe, StateData), - {Acc1, StateData}; - <<"unsubscribed">> -> - Acc1 = process_presence_subscription_and_route(Acc, unsubscribed, StateData), - {Acc1, StateData}; - <<"error">> -> - Acc1 = check_privacy_and_route(Acc, StateData), - {Acc1, StateData}; - <<"probe">> -> - Acc1 = check_privacy_and_route(Acc, StateData), - {Acc1, StateData}; - _ -> - Acc1 = check_privacy_and_route(Acc, StateData), - I = gb_sets:del_element(LTo, StateData#state.pres_i), - A = gb_sets:add_element(LTo, StateData#state.pres_a), - {Acc1, StateData#state{pres_i = I, - pres_a = A}} - end. - --spec process_presence_subscription_and_route(Acc :: mongoose_acc:t(), - Type :: subscribe | subscribed | unsubscribe | unsubscribed, - StateData :: state()) -> mongoose_acc:t(). -process_presence_subscription_and_route(Acc, Type, StateData) -> - From = mongoose_acc:from_jid(Acc), - To = mongoose_acc:to_jid(Acc), - Acc1 = mongoose_hooks:roster_out_subscription(Acc, From, To, Type), - check_privacy_and_route(Acc1, jid:to_bare(From), StateData). - --spec check_privacy_and_route(Acc :: mongoose_acc:t(), - StateData :: state()) -> mongoose_acc:t(). -check_privacy_and_route(Acc, StateData) -> - check_privacy_and_route(Acc, mongoose_acc:from_jid(Acc), StateData). - --spec check_privacy_and_route(Acc :: mongoose_acc:t(), - FromRoute :: jid:jid(), - StateData :: state()) -> mongoose_acc:t(). -check_privacy_and_route(Acc, FromRoute, StateData) -> - From = mongoose_acc:from_jid(Acc), - To = mongoose_acc:to_jid(Acc), - {Res, Acc1} = s_privacy_check_packet(Acc, To, out, StateData), - Packet = mongoose_acc:element(Acc1), - case Res of - deny -> - {Acc2, Err} = jlib:make_error_reply(Acc1, Packet, - mongoose_xmpp_errors:not_acceptable_cancel()), - ejabberd_router:route(To, From, Acc2, Err); - block -> - {Acc2, Err} = jlib:make_error_reply(Acc1, Packet, mongoose_xmpp_errors:not_acceptable_blocked()), - ejabberd_router:route(To, From, Acc2, Err); - allow -> - ejabberd_router:route(FromRoute, To, Acc1, Packet) - end. - - --spec p_privacy_check_packet(Packet :: exml:element(), - From :: jid:jid(), - To :: jid:jid(), - Dir :: 'in' | 'out', - StateData :: state()) -> allow|deny|block. -p_privacy_check_packet(#xmlel{} = Packet, From, To, Dir, StateData) -> - % in some cases we need an accumulator-less privacy check - Acc = new_acc(StateData, #{location => ?LOCATION, - from_jid => From, - to_jid => To, - element => Packet}), - {Res, _} = s_privacy_check_packet(Acc, To, Dir, StateData), - Res. - --spec s_privacy_check_packet(Acc :: mongoose_acc:t(), - To :: jid:jid(), - Dir :: 'in' | 'out', - StateData :: state()) -> {allow|deny|block, mongoose_acc:t()}. -s_privacy_check_packet(Acc, To, Dir, StateData) -> - mongoose_privacy:privacy_check_packet(Acc, - StateData#state.jid, - StateData#state.privacy_list, - To, - Dir). - --spec d_privacy_check_packet(Acc :: mongoose_acc:t(), - Packet :: exml:element(), - From :: jid:jid(), - To :: jid:jid(), - Dir :: 'in' | 'out', - StateData :: state()) -> {allow|deny|block, mongoose_acc:t()}. -d_privacy_check_packet(Acc, Packet, From, To, Dir, StateData) -> - mongoose_privacy:privacy_check_packet({Acc, Packet}, - StateData#state.jid, - StateData#state.privacy_list, - From, - To, - Dir). - --spec presence_broadcast(Acc :: mongoose_acc:t(), - JIDSet :: jid_set(), - State :: state()) -> mongoose_acc:t(). -presence_broadcast(Acc, JIDSet, StateData) -> - From = mongoose_acc:from_jid(Acc), - lists:foldl(fun(JID, A) -> - FJID = jid:make(JID), - {Res, A1} = s_privacy_check_packet(A, FJID, out, StateData), - case Res of - allow -> - ejabberd_router:route(From, FJID, A1); - _ -> - A1 - end - end, Acc, gb_sets:to_list(JIDSet)). - --spec presence_broadcast_to_trusted(Acc :: mongoose_acc:t(), - State :: state(), - From :: 'undefined' | jid:jid(), - T :: jid_set(), - A :: jid_set(), - Packet :: exml:element()) -> mongoose_acc:t(). -presence_broadcast_to_trusted(Acc, StateData, From, T, A, Packet) -> - lists:foldl( - fun(JID, Ac) -> - case gb_sets:is_element(JID, T) of - true -> - FJID = jid:make(JID), - check_privacy_and_route_or_ignore(Ac, StateData, From, FJID, Packet, out); - _ -> - Ac - end - end, Acc, gb_sets:to_list(A)). - --spec presence_broadcast_first(mongoose_acc:t(), - From :: 'undefined' | jid:jid(), - State :: state(), - Packet :: exml:element()) -> {mongoose_acc:t(), state()}. -presence_broadcast_first(Acc0, From, StateData, Packet) -> - Stanza = #xmlel{name = <<"presence">>, - attrs = [{<<"type">>, <<"probe">>}]}, - Acc = gb_sets:fold(fun(JID, A) -> - ejabberd_router:route(From, jid:make(JID), A, Stanza) - end, - Acc0, - StateData#state.pres_t), - case StateData#state.pres_invis of - true -> - {Acc, StateData}; - false -> - {As, AccFinal} = gb_sets:fold( - fun(JID, {A, Accum}) -> - FJID = jid:make(JID), - Accum1 = check_privacy_and_route_or_ignore(Accum, StateData, From, FJID, - Packet, out), - {gb_sets:add_element(JID, A), Accum1} - end, - {StateData#state.pres_a, Acc}, - StateData#state.pres_f), - {AccFinal, StateData#state{pres_a = As}} - end. - --spec roster_change(Acc :: mongoose_acc:t(), - IJID :: jid:simple_jid() | jid:jid(), - ISubscription :: from | to | both | none, - State :: state()) -> {mongoose_acc:t(), state()}. -roster_change(Acc, IJID, ISubscription, StateData) -> - LIJID = jid:to_lower(IJID), - IsSubscribedToMe = (ISubscription == both) or (ISubscription == from), - AmISubscribedTo = (ISubscription == both) or (ISubscription == to), - WasSubscribedToMe = gb_sets:is_element(LIJID, StateData#state.pres_f), - FSet = case IsSubscribedToMe of - true -> - gb_sets:add_element(LIJID, StateData#state.pres_f); - false -> - gb_sets:del_element(LIJID, StateData#state.pres_f) - end, - TSet = case AmISubscribedTo of - true -> - gb_sets:add_element(LIJID, StateData#state.pres_t); - false -> - gb_sets:del_element(LIJID, StateData#state.pres_t) - end, - case StateData#state.pres_last of - undefined -> - {Acc, StateData#state{pres_f = FSet, pres_t = TSet}}; - P -> - ?LOG_DEBUG(#{what => roster_changed, roster_jid => LIJID, - acc => Acc, c2s_state => StateData}), - From = StateData#state.jid, - To = jid:make(IJID), - IsntInvisible = not StateData#state.pres_invis, - ImAvailableTo = gb_sets:is_element(LIJID, StateData#state.pres_a), - ImInvisibleTo = gb_sets:is_element(LIJID, StateData#state.pres_i), - BecomeAvailable = IsntInvisible and IsSubscribedToMe and not WasSubscribedToMe, - BecomeUnavailable = not IsSubscribedToMe and WasSubscribedToMe - and (ImAvailableTo or ImInvisibleTo), - case {BecomeAvailable, BecomeUnavailable} of - {true, _} -> - ?LOG_DEBUG(#{what => become_available_to, roster_jid => LIJID, - acc => Acc, c2s_state => StateData}), - Acc1 = check_privacy_and_route_or_ignore(Acc, StateData, From, To, P, out), - A = gb_sets:add_element(LIJID, - StateData#state.pres_a), - NState = StateData#state{pres_a = A, - pres_f = FSet, - pres_t = TSet}, - {Acc1, NState}; - {_, true} -> - ?LOG_DEBUG(#{what => become_unavailable_to, roster_jid => LIJID, - acc => Acc, c2s_state => StateData}), - PU = #xmlel{name = <<"presence">>, - attrs = [{<<"type">>, <<"unavailable">>}]}, - Acc1 = check_privacy_and_route_or_ignore(Acc, StateData, From, To, PU, out), - I = gb_sets:del_element(LIJID, - StateData#state.pres_i), - A = gb_sets:del_element(LIJID, - StateData#state.pres_a), - NState = StateData#state{pres_i = I, - pres_a = A, - pres_f = FSet, - pres_t = TSet}, - {Acc1, NState}; - _ -> - {Acc, StateData#state{pres_f = FSet, pres_t = TSet}} - end - end. - --spec update_priority(Acc :: mongoose_acc:t(), - Priority :: integer(), - Packet :: exml:element(), - State :: state()) -> mongoose_acc:t(). -update_priority(Acc, Priority, Packet, StateData) -> - Info = #{ip => StateData#state.ip, conn => StateData#state.conn, - auth_module => StateData#state.auth_module }, - ejabberd_sm:set_presence(Acc, - StateData#state.sid, - StateData#state.jid, - Priority, - Packet, - Info). - - --spec get_priority_from_presence(Packet :: exml:element()) -> integer(). -get_priority_from_presence(undefined) -> - 0; -get_priority_from_presence(PresencePacket) -> - case xml:get_subtag(PresencePacket, <<"priority">>) of - false -> - 0; - SubEl -> - try binary_to_integer(xml:get_tag_cdata(SubEl)) of - P when is_integer(P) -> P - catch - error:badarg -> 0 - end - end. - --spec process_privacy_iq(Acc :: mongoose_acc:t(), - To :: jid:jid(), - StateData :: state()) -> {mongoose_acc:t(), state()}. -process_privacy_iq(Acc1, To, StateData) -> - case mongoose_iq:info(Acc1) of - {#iq{type = Type, sub_el = SubEl} = IQ, Acc2} when Type == get; Type == set -> - From = mongoose_acc:from_jid(Acc2), - {Acc3, NewStateData} = process_privacy_iq(Acc2, Type, To, StateData), - Res = mongoose_acc:get(hook, result, - {error, mongoose_xmpp_errors:feature_not_implemented( - <<"en">>, <<"Failed to handle the privacy IQ request in c2s">>)}, Acc3), - IQRes = case Res of - {result, Result} -> - IQ#iq{type = result, sub_el = Result}; - {result, Result, _} -> - IQ#iq{type = result, sub_el = Result}; - {error, Error} -> - IQ#iq{type = error, sub_el = [SubEl, Error]} - end, - Acc4 = ejabberd_router:route(To, From, Acc3, jlib:iq_to_xml(IQRes)), - {Acc4, NewStateData}; - _ -> - {Acc1, StateData} - end. - --spec process_privacy_iq(Acc :: mongoose_acc:t(), - Type :: get | set, - To :: jid:jid(), - StateData :: state()) -> {mongoose_acc:t(), state()}. -process_privacy_iq(Acc, get, To, StateData) -> - From = mongoose_acc:from_jid(Acc), - {IQ, Acc1} = mongoose_iq:info(Acc), - Acc2 = mongoose_hooks:privacy_iq_get(StateData#state.host_type, Acc1, - From, To, IQ, StateData#state.privacy_list), - {Acc2, StateData}; -process_privacy_iq(Acc, set, To, StateData) -> - From = mongoose_acc:from_jid(Acc), - {IQ, Acc1} = mongoose_iq:info(Acc), - Acc2 = mongoose_hooks:privacy_iq_set(StateData#state.host_type, Acc1, - From, To, IQ), - case mongoose_acc:get(hook, result, undefined, Acc2) of - {result, _, NewPrivList} -> - maybe_update_presence(Acc2, StateData, NewPrivList), - NState = StateData#state{privacy_list = NewPrivList}, - {Acc2, NState}; - _ -> {Acc2, StateData} - end. - - --spec resend_offline_messages(mongoose_acc:t(), state()) -> mongoose_acc:t(). -resend_offline_messages(Acc, StateData) -> - ?LOG_DEBUG(#{what => resend_offline_messages, - acc => Acc, c2s_state => StateData}), - Acc1 = mongoose_hooks:resend_offline_messages_hook(Acc, StateData#state.jid), - Rs = mongoose_acc:get(offline, messages, [], Acc1), - Acc2 = lists:foldl( - fun({route, From, To, MsgAcc}, A) -> - resend_offline_message(A, StateData, From, To, MsgAcc, in) - end, - Acc1, - Rs), - mongoose_acc:delete(offline, messages, Acc2). % they are gone from db backend and sent - - -resend_offline_message(Acc0, StateData, From, To, Acc, in) -> - Packet = mongoose_acc:element(Acc), - NewAcc = strip_c2s_fields(Acc), - check_privacy_and_route_or_ignore(NewAcc, StateData, From, To, Packet, in), - Acc0. - - --spec check_privacy_and_route_or_ignore(Acc :: mongoose_acc:t(), - StateData :: state(), - From :: jid:jid(), - To :: jid:jid(), - Packet :: exml:element(), - Dir :: in | out) -> any(). -check_privacy_and_route_or_ignore(Acc, StateData, From, To, Packet, Dir) -> - {Res, Acc2} = d_privacy_check_packet(Acc, Packet, From, To, Dir, StateData), - case Res of - allow -> ejabberd_router:route(From, To, Acc2, Packet); - _ -> Acc2 - end. - --spec resend_subscription_requests(mongoose_acc:t(), state()) -> {mongoose_acc:t(), state()}. -resend_subscription_requests(Acc, #state{pending_invitations = Pending} = StateData) -> - {NewAcc, NewState} = lists:foldl( - fun(XMLPacket, {A, #state{} = State}) -> - A1 = send_element(A, XMLPacket, State), - % We retrieve From i To from a stanza, because Acc has - % from_jid and to_jid that apply to 'available' stanza sent - % by the client - {value, From} = xml:get_tag_attr(<<"from">>, XMLPacket), - {value, To} = xml:get_tag_attr(<<"to">>, XMLPacket), - PacketTuple = {jid:from_binary(From), jid:from_binary(To), XMLPacket}, - BufferedStateData = buffer_out_stanza(A1, PacketTuple, State), - % this one will be next to tackle - A2 = maybe_send_ack_request(A1, BufferedStateData), - {A2, BufferedStateData} - end, {Acc, StateData}, Pending), - {NewAcc, NewState#state{pending_invitations = []}}. - - -get_showtag(undefined) -> - <<"unavailable">>; -get_showtag(Presence) -> - case xml:get_path_s(Presence, [{elem, <<"show">>}, cdata]) of - <<>> -> <<"available">>; - ShowTag -> ShowTag - end. - - -get_statustag(undefined) -> - <<>>; -get_statustag(Presence) -> - case xml:get_path_s(Presence, [{elem, <<"status">>}, cdata]) of - ShowTag -> ShowTag - end. - - --spec process_unauthenticated_stanza(State :: state(), - El :: exml:element()) -> any(). -process_unauthenticated_stanza(StateData, El) -> - NewEl = case xml:get_tag_attr_s(<<"xml:lang">>, El) of - <<>> -> - case StateData#state.lang of - <<>> -> El; - L -> - xml:replace_tag_attr(<<"xml:lang">>, L, El) - end; - _ -> - El - end, - Lang = xml:get_tag_attr_s(<<"xml:lang">>, NewEl), - case jlib:iq_query_info(NewEl) of - #iq{} = IQ -> - Res = mongoose_hooks:c2s_unauthenticated_iq( - StateData#state.host_type, - StateData#state.server, - IQ, StateData#state.ip), - case Res of - empty -> - % The only reasonable IQ's here are auth and register IQ's - % They contain secrets, so don't include subelements to response - Text = <<"Forbidden unauthenticated stanza">>, - ResIQ = IQ#iq{type = error, - sub_el = [mongoose_xmpp_errors:service_unavailable(Lang, Text)]}, - Res1 = jlib:replace_from_to( - jid:make_noprep(<<>>, StateData#state.server, <<>>), - jid:make_noprep(<<>>, <<>>, <<>>), - jlib:iq_to_xml(ResIQ)), - send_element_from_server_jid(StateData, jlib:remove_attr(<<"to">>, Res1)); - _ -> - send_element_from_server_jid(StateData, Res) - end; - _ -> - % Drop any stanza, which isn't IQ stanza - ok - end. - - --spec peerip(SockMod :: ejabberd:sockmod(), inet:socket()) -> - undefined | {inet:ip_address(), inet:port_number()}. -peerip(SockMod, Socket) -> - case mongoose_transport:peername(SockMod, Socket) of - {ok, IPOK} -> IPOK; - _ -> undefined - end. - - -%% @doc fsm_next_state_pack: Pack the StateData structure to improve sharing. -fsm_next_state_pack(StateName, StateData) -> - fsm_next_state_gc(StateName, pack(StateData)). - - -%% @doc fsm_next_state_gc: Garbage collect the process heap to make use of -%% the newly packed StateData structure. -fsm_next_state_gc(StateName, PackedStateData) -> - erlang:garbage_collect(), - fsm_next_state(StateName, PackedStateData). - - -%% @doc fsm_next_state: Generate the next_state FSM tuple with different -%% timeout, depending on the future state -fsm_next_state(session_established, StateData) -> - {next_state, session_established, StateData, maybe_hibernate(StateData)}; -fsm_next_state(StateName, StateData) -> - {next_state, StateName, StateData, ?C2S_OPEN_TIMEOUT}. - - -%% @doc fsm_reply: Generate the reply FSM tuple with different timeout, -%% depending on the future state -fsm_reply(Reply, session_established, StateData) -> - {reply, Reply, session_established, StateData, maybe_hibernate(StateData)}; -fsm_reply(Reply, StateName, StateData) -> - {reply, Reply, StateName, StateData, ?C2S_OPEN_TIMEOUT}. - - -%% @doc Used by c2s blacklist plugins --spec is_ip_blacklisted('undefined' | {inet:ip_address(), inet:port_number()} - ) -> boolean(). -is_ip_blacklisted(undefined) -> - false; -is_ip_blacklisted({IP, _Port}) -> - mongoose_hooks:check_bl_c2s(IP). - - -%% @doc Check from attributes. --spec check_from(El, C2SJID) -> Result when - El :: exml:element(), C2SJID :: jid:jid(), - Result :: 'invalid-from' | exml:element(). -check_from(El, #jid{ luser = C2SU, lserver = C2SS, lresource = C2SR }) -> - case xml:get_tag_attr(<<"from">>, El) of - false -> - El; - {value, SJID} -> - case jid:from_binary(SJID) of - #jid{ luser = U, lserver = S, lresource = R } - when U == C2SU andalso S == C2SS andalso (R == C2SR orelse R == <<>>) -> El; - _ -> 'invalid-from' - end - end. - --spec fsm_limit_opts(options()) -> [{max_queue, integer()}]. -fsm_limit_opts(#{max_fsm_queue := N}) -> - [{max_queue, N}]; -fsm_limit_opts(#{}) -> - case mongoose_config:lookup_opt(max_fsm_queue) of - {ok, N} -> - [{max_queue, N}]; - {error, not_found} -> - [] - end. - --spec bounce_messages(list(), #state{}) -> 'ok'. -bounce_messages(UnreadMessages, StateData) -> - case get_msg(UnreadMessages) of - {ok, {route, From, To, Acc}, RemainedUnreadMessages} -> - reroute(From, To, Acc, StateData), - bounce_messages(RemainedUnreadMessages, StateData); - {ok, {store_session_info, JID, Key, Value, _FromPid}, _} -> - ejabberd_sm:store_info(JID, Key, Value); - {ok, _, RemainedUnreadMessages} -> - % ignore this one, get the next message - bounce_messages(RemainedUnreadMessages, StateData); - {error, no_messages} -> - ok - end. - -%% Return the messages in reverse order than they were received in! --spec flush_messages(list()) -> [mongoose_acc:t()]. -flush_messages(UnreadMessages) -> - flush_messages([], UnreadMessages). - --spec flush_messages([mongoose_acc:t()], list()) -> [mongoose_acc:t()]. -flush_messages(Acc, UnreadMessages) -> - case get_msg(UnreadMessages) of - {ok, {route, From, To, MongooseAcc}, RemainedUnreadMessages} -> - El = mongoose_acc:element(MongooseAcc), - NewMongooseAcc = update_stanza(From, To, El, MongooseAcc), - NewAcc = [NewMongooseAcc | Acc], - flush_messages(NewAcc, RemainedUnreadMessages); - {ok, _, RemainedUnreadMessages} -> - % ignore this one, get the next message - flush_messages(Acc, RemainedUnreadMessages); - {error, no_messages} -> - Acc - end. - -get_msg([H | T]) -> - {ok, H, T}; -get_msg([]) -> - receive - Msg -> {ok, Msg, []} - after 0 -> - {error, no_messages} - end. - -%%%---------------------------------------------------------------------- -%%% XEP-0016 -%%%---------------------------------------------------------------------- - -maybe_update_presence(Acc, StateData = #state{jid = JID, pres_f = Froms}, NewList) -> - % Our own jid is added to pres_f, even though we're not a "contact", so for - % the purposes of this check we don't want it: - SelfJID = jid:to_lower(jid:to_bare(JID)), - FromsExceptSelf = gb_sets:del_element(SelfJID, Froms), - - gb_sets:fold( - fun(T, Ac) -> - send_unavail_if_newly_blocked(Ac, StateData, jid:make(T), NewList) - end, Acc, FromsExceptSelf). - -send_unavail_if_newly_blocked(Acc, StateData = #state{jid = JID}, - ContactJID, NewList) -> - Packet = #xmlel{name = <<"presence">>, - attrs = [{<<"type">>, <<"unavailable">>}]}, - %% WARNING: we can not use accumulator to cache privacy check result - this is - %% the only place where the list to check against changes - OldResult = p_privacy_check_packet(Packet, JID, ContactJID, out, StateData), - NewResult = p_privacy_check_packet(Packet, JID, ContactJID, out, - StateData#state{privacy_list = NewList}), - send_unavail_if_newly_blocked(Acc, OldResult, NewResult, JID, - ContactJID, Packet). - -send_unavail_if_newly_blocked(Acc, allow, deny, From, To, Packet) -> - ejabberd_router:route(From, To, Acc, Packet); -send_unavail_if_newly_blocked(Acc, _, _, _, _, _) -> - Acc. - -%%%---------------------------------------------------------------------- -%%% XEP-0191 -%%%---------------------------------------------------------------------- - --spec blocking_push_to_resources(Action :: blocking_type(), - JIDS :: [binary()], - State :: state()) -> ok. -blocking_push_to_resources(Action, JIDs, StateData) -> - SubEl = - case Action of - block -> - #xmlel{name = <<"block">>, - attrs = [{<<"xmlns">>, ?NS_BLOCKING}], - children = lists:map( - fun(JID) -> - #xmlel{name = <<"item">>, - attrs = [{<<"jid">>, JID}]} - end, JIDs)}; - unblock -> - #xmlel{name = <<"unblock">>, - attrs = [{<<"xmlns">>, ?NS_BLOCKING}], - children = lists:map( - fun(JID) -> - #xmlel{name = <<"item">>, - attrs = [{<<"jid">>, JID}]} - end, JIDs)} - end, - PrivPushIQ = #iq{type = set, xmlns = ?NS_BLOCKING, - id = <<"push">>, - sub_el = [SubEl]}, - F = jid:to_bare(StateData#state.jid), - T = StateData#state.jid, - PrivPushEl = jlib:replace_from_to(F, T, jlib:iq_to_xml(PrivPushIQ)), - ejabberd_router:route(F, T, PrivPushEl), - ok. - --spec blocking_presence_to_contacts(Action :: blocking_type(), - JIDs :: [binary()], - State :: state()) -> ok. -blocking_presence_to_contacts(_Action, [], _StateData) -> - ok; -blocking_presence_to_contacts(Action, [Jid|JIDs], StateData) -> - Pres = case Action of - block -> - #xmlel{name = <<"presence">>, - attrs = [{<<"xml:lang">>, <<"en">>}, {<<"type">>, <<"unavailable">>}] - }; - unblock -> - StateData#state.pres_last - end, - T = jid:from_binary(Jid), - case is_subscribed_to_my_presence(T, StateData) of - true -> - F = jid:to_bare(StateData#state.jid), - ejabberd_router:route(F, T, Pres); - false -> - ok - end, - blocking_presence_to_contacts(Action, JIDs, StateData). - --type pack_tree() :: gb_trees:tree(binary() | jid:simple_jid(), - binary() | jid:simple_jid()). - -%% @doc Try to reduce the heap footprint of the four presence sets -%% by ensuring that we re-use strings and Jids wherever possible. --spec pack(S :: state()) -> state(). -pack(S = #state{pres_a=A, - pres_i=I, - pres_f=F, - pres_t=T}) -> - {NewA, Pack1} = pack_jid_set(A, gb_trees:empty()), - {NewI, Pack2} = pack_jid_set(I, Pack1), - {NewF, Pack3} = pack_jid_set(F, Pack2), - {NewT, _Pack4} = pack_jid_set(T, Pack3), - %% Throw away Pack4 so that if we delete references to - %% Strings or Jids in any of the sets there will be - %% no live references for the GC to find. - S#state{pres_a=NewA, - pres_i=NewI, - pres_f=NewF, - pres_t=NewT}. - - --spec pack_jid_set(Set :: jid_set(), - Pack :: pack_tree()) -> {jid_set(), pack_tree()}. -pack_jid_set(Set, Pack) -> - Jids = gb_sets:to_list(Set), - {PackedJids, NewPack} = pack_jids(Jids, Pack, []), - {gb_sets:from_list(PackedJids), NewPack}. - - --spec pack_jids([{_, _, _}], Pack :: pack_tree(), Acc :: [jid:simple_jid()]) -> - {[jid:simple_jid()], pack_tree()}. -pack_jids([], Pack, Acc) -> {Acc, Pack}; -pack_jids([{U, S, R}=Jid | Jids], Pack, Acc) -> - case gb_trees:lookup(Jid, Pack) of - {value, PackedJid} -> - pack_jids(Jids, Pack, [PackedJid | Acc]); - none -> - {NewU, Pack1} = pack_string(U, Pack), - {NewS, Pack2} = pack_string(S, Pack1), - {NewR, Pack3} = pack_string(R, Pack2), - NewJid = {NewU, NewS, NewR}, - NewPack = gb_trees:insert(NewJid, NewJid, Pack3), - pack_jids(Jids, NewPack, [NewJid | Acc]) - end. - - --spec pack_string(String :: binary(), Pack :: pack_tree()) -> {binary(), pack_tree()}. -pack_string(String, Pack) -> - case gb_trees:lookup(String, Pack) of - {value, PackedString} -> - {PackedString, Pack}; - none -> - {String, gb_trees:insert(String, String, Pack)} - end. - -%%%---------------------------------------------------------------------- -%%% XEP-0352: Client State Indication -%%%---------------------------------------------------------------------- -maybe_inactivate_session(?NS_CSI, #state{csi_state = active} = State) -> - fsm_next_state(session_established, State#state{csi_state = inactive}); -maybe_inactivate_session(_, State) -> - fsm_next_state(session_established, State). - -maybe_activate_session(?NS_CSI, #state{csi_state = inactive} = State) -> - resend_csi_buffer(State); -maybe_activate_session(_, State) -> - fsm_next_state(session_established, State). - -resend_csi_buffer(State) -> - NewState = flush_csi_buffer(State), - fsm_next_state(session_established, NewState#state{csi_state=active}). - --spec ship_to_local_user(mongoose_acc:t(), packet(), state()) -> - {ok | resume, mongoose_acc:t(), state()}. -ship_to_local_user(Acc, Packet, State) -> - maybe_csi_inactive_optimisation(Acc, Packet, State). - --spec maybe_csi_inactive_optimisation(mongoose_acc:t(), packet(), state()) -> - {ok | resume, mongoose_acc:t(), state()}. -maybe_csi_inactive_optimisation(Acc, Packet, #state{csi_state = active} = State) -> - send_and_maybe_buffer_stanza(Acc, Packet, State); -maybe_csi_inactive_optimisation(Acc, {From,To,El}, #state{csi_buffer = Buffer} = State) -> - NewAcc = update_stanza(From, To, El, Acc), - NewBuffer = [NewAcc | Buffer], - NewState = flush_or_buffer_packets(State#state{csi_buffer = NewBuffer}), - {ok, Acc, NewState}. - -flush_or_buffer_packets(State) -> - MaxBuffSize = gen_mod:get_module_opt(State#state.host_type, mod_csi, buffer_max), - case length(State#state.csi_buffer) > MaxBuffSize of - true -> - flush_csi_buffer(State); - _ -> - State - end. - --spec flush_csi_buffer(state()) -> state(). -flush_csi_buffer(#state{csi_buffer = BufferOut} = State) -> - %%lists:foldr to preserve order - F = fun(Acc, {_, _, OldState}) -> - {From, To, El} = mongoose_acc:packet(Acc), - send_and_maybe_buffer_stanza_no_ack(Acc, {From, To, El}, OldState) - end, - {_, _, NewState} = lists:foldr(F, {ok, ok, State}, BufferOut), - NewState#state{csi_buffer = []}. - -bounce_csi_buffer(#state{csi_buffer = []}) -> - ok; -bounce_csi_buffer(#state{csi_buffer = Buffer} = State) -> - re_route_packets(Buffer, State). - -%%%---------------------------------------------------------------------- -%%% XEP-0198: Stream Management -%%%---------------------------------------------------------------------- -maybe_enable_stream_mgmt(NextState, El, StateData = #state{host_type = HostType}) -> - case {xml:get_tag_attr_s(<<"xmlns">>, El), - StateData#state.stream_mgmt, - xml:get_tag_attr_s(<<"resume">>, El)} - of - {?NS_STREAM_MGNT_3, false, Resume} -> - %% turn on - {NewSD, EnabledEl} = case lists:member(Resume, [<<"true">>, <<"1">>]) of - false -> - {StateData, stream_mgmt_enabled()}; - true -> - enable_stream_resumption(StateData) - end, - send_element_from_server_jid(NewSD, EnabledEl), - BufferMax = mod_stream_management:get_buffer_max(HostType), - AckFreq = mod_stream_management:get_ack_freq(HostType), - ResumeTimeout = mod_stream_management:get_resume_timeout(HostType), - fsm_next_state(NextState, - NewSD#state{stream_mgmt = true, - stream_mgmt_buffer_max = BufferMax, - stream_mgmt_ack_freq = AckFreq, - stream_mgmt_resume_timeout = ResumeTimeout}); - {?NS_STREAM_MGNT_3, true, _} -> - c2s_stream_error(stream_mgmt_failed(<<"unexpected-request">>), StateData); - {?NS_STREAM_MGNT_3, disabled, _} -> - c2s_stream_error(stream_mgmt_failed(<<"feature-not-implemented">>), StateData); - {_, _, _} -> - %% invalid namespace - c2s_stream_error(mongoose_xmpp_errors:invalid_namespace(), StateData) - end. - -enable_stream_resumption(SD = #state{host_type = HostType}) -> - SMID = mod_stream_management:make_smid(), - SID = case SD#state.sid of - undefined -> ejabberd_sm:make_new_sid(); - RSID -> RSID - end, - ok = mod_stream_management:register_smid(HostType, SMID, SID), - {SD#state{stream_mgmt_id = SMID, sid = SID}, - stream_mgmt_enabled([{<<"id">>, SMID}, {<<"resume">>, <<"true">>}])}. - -maybe_unexpected_sm_request(NextState, El, StateData) -> - case xml:get_tag_attr_s(<<"xmlns">>, El) of - ?NS_STREAM_MGNT_3 -> - send_element_from_server_jid(StateData, stream_mgmt_failed(<<"unexpected-request">>)), - fsm_next_state(NextState, StateData); - _ -> - c2s_stream_error(mongoose_xmpp_errors:invalid_namespace(), StateData) - end. - -stream_mgmt_handle_ack(NextState, El, #state{} = SD) -> - case {exml_query:attr(El, <<"xmlns">>), stream_mgmt_parse_h(El)} of - {NS, _} when NS =/= ?NS_STREAM_MGNT_3 -> - maybe_send_element_from_server_jid_safe(SD, mongoose_xmpp_errors:invalid_namespace()), - maybe_send_trailer_safe(SD), - {stop, normal, SD}; - {_, invalid_h_attribute} -> - PolicyViolationErr = mongoose_xmpp_errors:policy_violation( - SD#state.lang, <<"Invalid h attribute">>), - maybe_send_element_from_server_jid_safe(SD, PolicyViolationErr), - maybe_send_trailer_safe(SD), - {stop, normal, SD}; - {_, Handled} -> - try - NSD = #state{} = do_handle_ack(Handled, - SD#state.stream_mgmt_out_acked, - SD#state.stream_mgmt_buffer, - SD#state.stream_mgmt_buffer_size, - SD), - fsm_next_state(NextState, NSD) - catch - throw:{undefined_condition, H, OldAcked} -> - #xmlel{children = [UndefCond, Text]} = ErrorStanza0 - = mongoose_xmpp_errors:undefined_condition( - SD#state.lang, <<"You acknowledged more stanzas that what has been sent">>), - HandledCountField = sm_handled_count_too_high_stanza(H, OldAcked), - ErrorStanza = ErrorStanza0#xmlel{children = [UndefCond, HandledCountField, Text]}, - maybe_send_element_from_server_jid_safe(SD, ErrorStanza), - maybe_send_trailer_safe(SD), - {stop, normal, SD} - end - end. - -stream_mgmt_parse_h(El) -> - case catch binary_to_integer(exml_query:attr(El, <<"h">>)) of - H when is_integer(H) -> H; - _ -> invalid_h_attribute - end. - -do_handle_ack(Handled, OldAcked, Buffer, BufferSize, SD) -> - ToDrop = calc_to_drop(Handled, OldAcked), - ToDrop > BufferSize andalso throw({undefined_condition, Handled, OldAcked}), - {Dropped, NewBuffer} = drop_last(ToDrop, Buffer), - NewSize = BufferSize - Dropped, - SD#state{stream_mgmt_out_acked = Handled, - stream_mgmt_buffer = NewBuffer, - stream_mgmt_buffer_size = NewSize}. - -calc_to_drop(Handled, OldAcked) when Handled >= OldAcked -> - Handled - OldAcked; -calc_to_drop(Handled, OldAcked) -> - Handled + ?STREAM_MGMT_H_MAX - OldAcked + 1. - -maybe_send_sm_ack(?NS_STREAM_MGNT_3, StreamMgmt, _NIncoming, NextState, StateData) - when StreamMgmt =:= false; StreamMgmt =:= disabled -> - ?LOG_WARNING(#{what => unexpected_r, c2s_state => StateData, - text => <<"received but stream management is off!">>}), - fsm_next_state(NextState, StateData); -maybe_send_sm_ack(?NS_STREAM_MGNT_3, true, NIncoming, - NextState, StateData) -> - send_element_from_server_jid(StateData, stream_mgmt_ack(NIncoming)), - fsm_next_state(NextState, StateData); -maybe_send_sm_ack(_, _, _, _NextState, StateData) -> - c2s_stream_error(mongoose_xmpp_errors:invalid_namespace(), StateData). - -maybe_increment_sm_incoming(StreamMgmt, StateData) - when StreamMgmt =:= false; StreamMgmt =:= disabled -> - StateData; -maybe_increment_sm_incoming(true, StateData) -> - Incoming = StateData#state.stream_mgmt_in, - StateData#state{stream_mgmt_in = increment_sm_incoming(Incoming)}. - -increment_sm_incoming(Incoming) -> - increment_sm_counter(Incoming, 1). - -increment_sm_counter(Incoming, Increment) - when Incoming + Increment >= ?STREAM_MGMT_H_MAX -> - Increment - 1; -increment_sm_counter(Incoming, Increment) -> - Incoming + Increment. - -stream_mgmt_enabled() -> - stream_mgmt_enabled([]). - -stream_mgmt_enabled(ExtraAttrs) -> - #xmlel{name = <<"enabled">>, - attrs = [{<<"xmlns">>, ?NS_STREAM_MGNT_3}] ++ ExtraAttrs}. - -stream_mgmt_failed(Reason) -> - stream_mgmt_failed(Reason, []). - -stream_mgmt_failed(Reason, Attrs) -> - ReasonEl = #xmlel{name = Reason, - attrs = [{<<"xmlns">>, ?NS_STANZAS}]}, - #xmlel{name = <<"failed">>, - attrs = [{<<"xmlns">>, ?NS_STREAM_MGNT_3} | Attrs], - children = [ReasonEl]}. - -stream_mgmt_ack(NIncoming) -> - #xmlel{name = <<"a">>, - attrs = [{<<"xmlns">>, ?NS_STREAM_MGNT_3}, - {<<"h">>, integer_to_binary(NIncoming)}]}. - --spec buffer_out_stanza(mongoose_acc:t(), packet(), state()) -> state(). -buffer_out_stanza(_Acc, _Packet, #state{stream_mgmt = StreamMgmt} = S) - when StreamMgmt =:= false; StreamMgmt =:= disabled -> - S; -buffer_out_stanza(_Acc, _Packet, #state{stream_mgmt_buffer_max = no_buffer} = S) -> - S; -buffer_out_stanza(Acc, Packet, #state{server = Server, - stream_mgmt_buffer = Buffer, - stream_mgmt_buffer_size = BufferSize, - stream_mgmt_buffer_max = BufferMax} = S) -> - NewSize = BufferSize + 1, - Timestamp = os:system_time(microsecond), - {From, To, El} = maybe_add_timestamp(Packet, Timestamp, Server), - Acc1 = update_stanza(From, To, El, Acc), - NS = case is_buffer_full(NewSize, BufferMax) of - true -> - defer_resource_constraint_check(S); - _ -> - S - end, - Acc2 = notify_unacknowledged_msg_if_in_resume_state(Acc1, NS), - NS#state{stream_mgmt_buffer_size = NewSize, - stream_mgmt_buffer = [Acc2 | Buffer]}. - -is_buffer_full(_BufferSize, infinity) -> - false; -is_buffer_full(BufferSize, BufferMax) when BufferSize =< BufferMax -> - false; -is_buffer_full(_, _) -> - true. - -%% @doc Drop last N elements from List. -%% It's not an error if N > length(List). -%% The actual number of dropped elements and an empty list is returned. -%% @end --spec drop_last(N, List1) -> {Dropped, List2} when - N :: non_neg_integer(), - List1 :: list(), - Dropped :: non_neg_integer(), - List2 :: list(). -drop_last(N, List) -> - {ToDrop, List2} = lists:foldr(fun(E, {0, Acc}) -> - {0, [E | Acc]}; - (_, {ToDrop, Acc}) -> - {ToDrop-1, Acc} - end, {N, []}, List), - {N - ToDrop, List2}. - -maybe_send_ack_request(Acc, #state{stream_mgmt = StreamMgmt}) - when StreamMgmt =:= false; StreamMgmt =:= disabled -> - Acc; -maybe_send_ack_request(Acc, #state{stream_mgmt_ack_freq = never}) -> - Acc; -maybe_send_ack_request(Acc, #state{stream_mgmt_out_acked = Out, - stream_mgmt_buffer_size = BufferSize, - stream_mgmt_ack_freq = AckFreq} = State) - when (Out + BufferSize) rem AckFreq == 0 -> - send_element(Acc, stream_mgmt_request(), State); -maybe_send_ack_request(Acc, _) -> - Acc. - -stream_mgmt_request() -> - #xmlel{name = <<"r">>, - attrs = [{<<"xmlns">>, ?NS_STREAM_MGNT_3}]}. - -flush_stream_mgmt_buffer(#state{stream_mgmt = StreamMgmt}) - when StreamMgmt =:= false; StreamMgmt =:= disabled -> - false; -flush_stream_mgmt_buffer(#state{stream_mgmt_buffer = Buffer} = State) -> - re_route_packets(Buffer, State). - -re_route_packets(Buffer, #state{jid = Jid, host_type = HostType} = StateData) -> - OrderedBuffer = lists:reverse(Buffer), - FilteredBuffer = mongoose_hooks:filter_unacknowledged_messages(HostType, Jid, OrderedBuffer), - [reroute(Acc, StateData) || Acc <- FilteredBuffer], - ok. - -reroute(Acc, StateData) -> - {From, To, _El} = mongoose_acc:packet(Acc), - reroute(From, To, Acc, StateData). - -reroute(From, To, Acc, #state{sid = SID}) -> - Acc2 = patch_acc_for_reroute(Acc, SID), - ejabberd_router:route(From, To, Acc2). - -patch_acc_for_reroute(Acc, SID) -> - case mongoose_acc:stanza_name(Acc) of - <<"message">> -> - Acc; - _ -> %% IQs and presences are allowed to come to the same SID only - case mongoose_acc:get(c2s, receiver_sid, undefined, Acc) of - undefined -> - mongoose_acc:set_permanent(c2s, receiver_sid, SID, Acc); - _ -> - Acc - end - end. - -notify_unacknowledged_messages(#state{stream_mgmt_buffer = Buffer} = State) -> - NewBuffer = [maybe_notify_unacknowledged_msg(Acc, State) || Acc <- lists:reverse(Buffer)], - State#state{stream_mgmt_buffer = lists:reverse(NewBuffer)}. - -notify_unacknowledged_msg_if_in_resume_state(Acc, - #state{stream_mgmt_resume_tref = TRef, - stream_mgmt = true} = State) when TRef =/= undefined -> - maybe_notify_unacknowledged_msg(Acc, State); -notify_unacknowledged_msg_if_in_resume_state(Acc, _) -> - Acc. - -maybe_notify_unacknowledged_msg(Acc, #state{jid = Jid}) -> - case mongoose_acc:stanza_name(Acc) of - <<"message">> -> notify_unacknowledged_msg(Acc, Jid); - _ -> Acc - end. - -notify_unacknowledged_msg(Acc, Jid) -> - NewAcc = mongoose_hooks:unacknowledged_message(Acc, Jid), - mongoose_acc:strip(NewAcc). - -finish_state(ok, StateName, StateData) -> - fsm_next_state(StateName, StateData); -finish_state(resume, _, StateData) -> - maybe_enter_resume_session(StateData). - -maybe_enter_resume_session(StateData) -> - maybe_enter_resume_session(StateData#state.stream_mgmt_id, StateData). - -maybe_enter_resume_session(undefined, StateData) -> - {stop, normal, StateData}; -maybe_enter_resume_session(_SMID, #state{} = SD) -> - NSD = case SD#state.stream_mgmt_resume_tref of - undefined -> - Seconds = timer:seconds(SD#state.stream_mgmt_resume_timeout), - TRef = erlang:send_after(Seconds, self(), resume_timeout), - NewState = SD#state{stream_mgmt_resume_tref = TRef}, - notify_unacknowledged_messages(NewState); - _TRef -> - SD - end, - {next_state, resume_session, NSD, hibernate()}. - -maybe_resume_session(NextState, El, StateData = #state{host_type = HostType}) -> - case {xml:get_tag_attr_s(<<"xmlns">>, El), - xml:get_tag_attr_s(<<"previd">>, El)} of - {?NS_STREAM_MGNT_3, SMID} -> - FromSMID = mod_stream_management:get_session_from_smid(HostType, SMID), - do_resume_session(SMID, El, FromSMID, StateData); - {InvalidNS, _} -> - ?LOG_INFO(#{what => c2s_ignores_resume, - text => <<"ignoring element with invalid namespace">>, - invalid_ns => InvalidNS, c2s_state => StateData}), - fsm_next_state(NextState, StateData) - end. - --spec do_resume_session(SMID, El, FromSMID, StateData) -> NewState when - SMID :: mod_stream_management:smid(), - El :: exml:element(), - FromSMID :: {sid, ejabberd_sm:sid()} | {stale_h, non_neg_integer()} | {error, smid_not_found}, - StateData :: state(), - NewState :: tuple(). -do_resume_session(SMID, El, {sid, {_, Pid}}, StateData) -> - try - {ok, OldState} = p1_fsm_old:sync_send_event(Pid, resume), - SID = ejabberd_sm:make_new_sid(), - Conn = get_conn_type(StateData), - MergedState = merge_state(OldState, - StateData#state{sid = SID, conn = Conn}), - case stream_mgmt_handle_ack(session_established, El, MergedState) of - {stop, _, _} = Stop -> - Stop; - {next_state, session_established, NSD, _} -> - Priority = get_priority_from_presence(NSD#state.pres_last), - Info = #{ip => NSD#state.ip, conn => NSD#state.conn, - auth_module => NSD#state.auth_module }, - ejabberd_sm:open_session(NSD#state.host_type, SID, NSD#state.jid, Priority, Info), - ok = mod_stream_management:register_smid(NSD#state.host_type, SMID, SID), - try - Resumed = stream_mgmt_resumed(NSD#state.stream_mgmt_id, - NSD#state.stream_mgmt_in), - send_element_from_server_jid(NSD, Resumed), - [begin - Elem = mongoose_acc:element(Acc), - send_element(Acc, Elem, NSD) - end || Acc <- lists:reverse(NSD#state.stream_mgmt_buffer)], - - NSD2 = flush_csi_buffer(NSD), - - NSD3 = NSD2#state{ stream_mgmt_resumed_from = OldState#state.sid }, - - fsm_next_state(session_established, NSD3) - catch - %% errors from send_element - _:_ -> - ?LOG_INFO(#{what => resumption_error, - text => <<"resumption error while resending old stanzas" - " entering resume state again">>, - smid => SMID, c2s_state => NSD}), - maybe_enter_resume_session(SMID, NSD) - end - end - catch - _Class:Reason:Stacktrace -> - ?LOG_WARNING(#{what => resumption_error, reason => invalid_response, - text => <<"Resumption error because of invalid response">>, - error => Reason, stacktrace => Stacktrace, - c2s_state => StateData}), - send_element_from_server_jid(StateData, stream_mgmt_failed(<<"item-not-found">>)), - fsm_next_state(wait_for_feature_after_auth, StateData) - end; - -do_resume_session(SMID, _El, {stale_h, H}, StateData) when is_integer(H) -> - ?LOG_INFO(#{what => resumption_error, reason => session_resumption_timed_out, - smid => SMID, stale_h => H, c2s_state => StateData}), - send_element_from_server_jid( - StateData, stream_mgmt_failed(<<"item-not-found">>, [{<<"h">>, integer_to_binary(H)}])), - fsm_next_state(wait_for_feature_after_auth, StateData); -do_resume_session(SMID, _El, {error, smid_not_found}, StateData) -> - ?LOG_INFO(#{what => resumption_error, reason => no_previous_session_for_smid, - smid => SMID, c2s_state => StateData}), - send_element_from_server_jid(StateData, stream_mgmt_failed(<<"item-not-found">>)), - fsm_next_state(wait_for_feature_after_auth, StateData). - -merge_state(OldSD, SD) -> - Preserve = [#state.jid, - #state.user, - #state.server, - #state.resource, - #state.pres_t, - #state.pres_f, - #state.pres_a, - #state.pres_i, - #state.pres_last, - #state.pres_pri, - #state.pres_timestamp, - #state.pres_invis, - #state.privacy_list, - #state.aux_fields, - #state.csi_buffer, - #state.stream_mgmt, - #state.stream_mgmt_in, - #state.stream_mgmt_id, - #state.stream_mgmt_out_acked, - #state.stream_mgmt_buffer, - #state.stream_mgmt_buffer_size, - #state.stream_mgmt_buffer_max, - #state.stream_mgmt_resume_timeout, - #state.stream_mgmt_ack_freq], - Copy = fun(Index, {Stale, Acc}) -> - {Stale, setelement(Index, Acc, element(Index, Stale))} - end, - element(2, lists:foldl(Copy, {OldSD, SD}, Preserve)). - -stream_mgmt_resumed(SMID, Handled) -> - #xmlel{name = <<"resumed">>, - attrs = [{<<"xmlns">>, ?NS_STREAM_MGNT_3}, - {<<"previd">>, SMID}, - {<<"h">>, integer_to_binary(Handled)}]}. - -handover_session(SD, From)-> - true = SD#state.stream_mgmt, - Acc = new_acc(SD, #{location => ?LOCATION, element => undefined}), - ejabberd_sm:close_session(Acc, - SD#state.sid, - SD#state.jid, - resumed), - %the actual handover to be done on termination - {stop, {handover_session, From}, SD}. - -do_handover_session(SD, UnreadMessages) -> - Messages = flush_messages(UnreadMessages), - NewCsiBuffer = Messages ++ SD#state.csi_buffer, - SD#state{authenticated = resumed, - csi_buffer = NewCsiBuffer}. - -maybe_add_timestamp({F, T, #xmlel{name= <<"message">>}=Packet}=PacketTuple, Timestamp, Server) -> - Type = xml:get_tag_attr_s(<<"type">>, Packet), - case Type of - <<"error">> -> - PacketTuple; - <<"headline">> -> - PacketTuple; - _ -> - {F, T, add_timestamp(Timestamp, Server, Packet)} - end; -maybe_add_timestamp(Packet, _Timestamp, _Server) -> - Packet. - -add_timestamp(TimeStamp, Server, Packet) -> - case xml:get_subtag(Packet, <<"delay">>) of - false -> - TimeStampXML = timestamp_xml(Server, TimeStamp), - xml:append_subtags(Packet, [TimeStampXML]); - _ -> - Packet - end. - -timestamp_xml(Server, Time) -> - FromJID = jid:make_noprep(<<>>, Server, <<>>), - TS = calendar:system_time_to_rfc3339(Time, [{offset, "Z"}, {unit, microsecond}]), - jlib:timestamp_to_xml(TS, FromJID, <<"SM Storage">>). - -defer_resource_constraint_check(#state{stream_mgmt_constraint_check_tref = undefined} = State)-> - Seconds = timer:seconds(?CONSTRAINT_CHECK_TIMEOUT), - TRef = erlang:send_after(Seconds, self(), check_buffer_full), - State#state{stream_mgmt_constraint_check_tref = TRef}; -defer_resource_constraint_check(State)-> - State. - --spec sm_handled_count_too_high_stanza(non_neg_integer(), non_neg_integer()) -> exml:element(). -sm_handled_count_too_high_stanza(Handled, OldAcked) -> - #xmlel{name = <<"handled-count-too-high">>, - attrs = [{<<"xmlns">>, ?NS_STREAM_MGNT_3}, - {<<"h">>, integer_to_binary(Handled)}, - {<<"send-count">>, integer_to_binary(OldAcked)}]}. - --spec sasl_success_stanza(any()) -> exml:element(). -sasl_success_stanza(ServerOut) -> - C = case ServerOut of - undefined -> []; - _ -> [#xmlcdata{content = jlib:encode_base64(ServerOut)}] - end, - #xmlel{name = <<"success">>, - attrs = [{<<"xmlns">>, ?NS_SASL}], - children = C}. - --spec sasl_failure_stanza(binary() | {binary(), iodata() | undefined}) -> exml:element(). -sasl_failure_stanza(Error) when is_binary(Error) -> - sasl_failure_stanza({Error, undefined}); -sasl_failure_stanza({Error, Text}) -> - #xmlel{name = <<"failure">>, - attrs = [{<<"xmlns">>, ?NS_SASL}], - children = [#xmlel{name = Error} | maybe_text_tag(Text)]}. - -maybe_text_tag(undefined) -> []; -maybe_text_tag(Text) -> - [#xmlel{name = <<"text">>, - children = [#xmlcdata{content = Text}]}]. - --spec sasl_challenge_stanza(any()) -> exml:element(). -sasl_challenge_stanza(Challenge) -> - #xmlel{name = <<"challenge">>, - attrs = [{<<"xmlns">>, ?NS_SASL}], - children = Challenge}. - -handle_sasl_success(State, Creds) -> - ServerOut = mongoose_credentials:get(Creds, sasl_success_response, undefined), - send_element_from_server_jid(State, sasl_success_stanza(ServerOut)), - User = mongoose_credentials:get(Creds, username), - AuthModule = mongoose_credentials:get(Creds, auth_module), - StreamID = new_id(), - Server = State#state.server, - NewState = State#state{ streamid = StreamID, - authenticated = true, - auth_module = AuthModule, - user = User, - jid = jid:make_bare(User, Server)}, - ?LOG_INFO(#{what => auth_success, text => <<"Accepted SASL authentication">>, - stream_id => StreamID, auth_module => AuthModule, - c2s_state => NewState}), - {wait_for_stream, NewState}. - -handle_sasl_step(#state{host_type = HostType, server = Server, socket = Sock} = State, - StepRes) -> - case StepRes of - {ok, Creds} -> - handle_sasl_success(State, Creds); - {continue, ServerOut, NewSASLState} -> - Challenge = [#xmlcdata{content = jlib:encode_base64(ServerOut)}], - send_element_from_server_jid(State, sasl_challenge_stanza(Challenge)), - {wait_for_sasl_response, State#state{sasl_state = NewSASLState}}; - {error, Error, Username} -> - IP = peerip(State#state.sockmod, Sock), - ?LOG_INFO(#{what => auth_failed, - text => <<"Failed SASL authentication">>, - user => Username, server => Server, - ip => IP, c2s_state => State}), - mongoose_hooks:auth_failed(HostType, Server, Username), - send_element_from_server_jid(State, sasl_failure_stanza(Error)), - {wait_for_feature_before_auth, State}; - {error, Error} -> - mongoose_hooks:auth_failed(HostType, Server, unknown), - send_element_from_server_jid(State, sasl_failure_stanza(Error)), - {wait_for_feature_before_auth, State} - end. - -user_allowed(JID, #state{host_type = HostType, server = Server, access = Access}) -> - case acl:match_rule(HostType, Server, Access, JID) of - allow -> - open_session_allowed_hook(HostType, JID); - deny -> - false - end. - -open_session_allowed_hook(HostType, JID) -> - allow == mongoose_hooks:session_opening_allowed_for_user(HostType, JID). - -terminate_when_tls_required_but_not_enabled(#state{tls_mode = starttls_required, - tls_enabled = false} = StateData, _El) -> - Lang = StateData#state.lang, - c2s_stream_error(mongoose_xmpp_errors:policy_violation(Lang, <<"Use of STARTTLS required">>), - StateData); -terminate_when_tls_required_but_not_enabled(StateData, El) -> - process_unauthenticated_stanza(StateData, El), - fsm_next_state(wait_for_feature_before_auth, StateData). - -%% @doc This function is executed when c2s receives a stanza from TCP connection. --spec element_to_origin_accum(jlib:xmlel(), StateData :: state()) -> - mongoose_acc:t(). -element_to_origin_accum(El, StateData = #state{sid = SID, jid = JID}) -> - BaseParams = #{ - location => ?LOCATION, - element => El, - from_jid => JID - }, - Params = - case exml_query:attr(El, <<"to">>) of - undefined -> BaseParams#{ to_jid => jid:to_bare(JID) }; - _ToBin -> BaseParams - end, - Acc = new_acc(StateData, Params), - C2S = [{origin_sid, SID}, {origin_jid, JID}], - mongoose_acc:set_permanent(c2s, C2S, Acc). - --spec hibernate() -> hibernate | infinity. -hibernate() -> - {_, QueueLen} = process_info(self(), message_queue_len), - InternalQueueLen = get('$internal_queue_len'), - case QueueLen + InternalQueueLen of - 0 -> hibernate; - _ -> infinity - end. - --spec maybe_hibernate(state()) -> hibernate | infinity | pos_integer(). -maybe_hibernate(#state{hibernate_after = 0}) -> hibernate(); -maybe_hibernate(#state{hibernate_after = HA}) -> HA. - -make_c2s_info(_StateData = #state{stream_mgmt_buffer_size = SMBufSize}) -> - #{stream_mgmt_buffer_size => SMBufSize}. - --spec update_stanza(jid:jid(), jid:jid(), exml:element(), mongoose_acc:t()) -> - mongoose_acc:t(). -update_stanza(From, To, #xmlel{} = Element, Acc) -> - HostType = mongoose_acc:host_type(Acc), - LServer = mongoose_acc:lserver(Acc), - Params = #{host_type => HostType, lserver => LServer, - element => Element, from_jid => From, to_jid => To}, - NewAcc = mongoose_acc:strip(Params, Acc), - strip_c2s_fields(NewAcc). - --spec strip_c2s_fields(mongoose_acc:t()) -> mongoose_acc:t(). -strip_c2s_fields(Acc) -> - %% TODO: verify if we really need to strip down these 2 fields - mongoose_acc:delete_many(c2s, [origin_jid, origin_sid], Acc). - --spec new_acc(state(), mongoose_acc:new_acc_params()) -> mongoose_acc:t(). -new_acc(#state{host_type = HostType, server = LServer}, Params) -> - mongoose_acc:new(Params#{host_type => HostType, lserver => LServer}). diff --git a/src/ejabberd_c2s_state.erl b/src/ejabberd_c2s_state.erl deleted file mode 100644 index 133abf68223..00000000000 --- a/src/ejabberd_c2s_state.erl +++ /dev/null @@ -1,17 +0,0 @@ --module(ejabberd_c2s_state). - --include("ejabberd_c2s.hrl"). - --export([server/1, jid/1, host_type/1]). - --spec server(state()) -> jid:lserver(). -server(#state{ server = Server }) -> - Server. - --spec host_type(state()) -> mongooseim:host_type(). -host_type(#state{ host_type = HostType }) -> - HostType. - --spec jid(state()) -> jid:jid(). -jid(#state{ jid = JID }) -> - JID. diff --git a/src/ejabberd_local.erl b/src/ejabberd_local.erl index 565ea0dd6af..d7b7787c114 100644 --- a/src/ejabberd_local.erl +++ b/src/ejabberd_local.erl @@ -279,7 +279,7 @@ init([]) -> %%-------------------------------------------------------------------- handle_call({unregister_host, Host}, _From, State) -> Node = node(), - [ejabberd_c2s:stop(Pid) + [mongoose_c2s:stop(Pid, host_was_unregistered) || #session{sid = {_, Pid}} <- ejabberd_sm:get_vh_session_list(Host), node(Pid) =:= Node], do_unregister_host(Host), diff --git a/src/ejabberd_router.erl b/src/ejabberd_router.erl index 1cd0a92d44d..239d4a226d9 100644 --- a/src/ejabberd_router.erl +++ b/src/ejabberd_router.erl @@ -380,7 +380,7 @@ routing_modules_list() -> To :: jid:jid(), Acc :: mongoose_acc:t(), Packet :: exml:element(), - [atom()]) -> mongoose_acc:t(). + [module()]) -> mongoose_acc:t(). route(_From, _To, Acc, _Packet, []) -> ?LOG_ERROR(#{what => no_more_routing_modules, acc => Acc}), mongoose_metrics:update(global, routingErrors, 1), diff --git a/src/ejabberd_service.erl b/src/ejabberd_service.erl index dd0a71bd99d..4761e57d697 100644 --- a/src/ejabberd_service.erl +++ b/src/ejabberd_service.erl @@ -155,8 +155,8 @@ start_listener(Opts) -> -spec process_packet(Acc :: mongoose_acc:t(), From :: jid:jid(), To :: jid:jid(), El :: exml:element(), #{pid := pid()}) -> mongoose_acc:t(). -process_packet(Acc, From, To, _El, #{pid := Pid}) -> - Pid ! {route, From, To, Acc}, +process_packet(Acc, _From, _To, _El, #{pid := Pid}) -> + Pid ! {route, Acc}, Acc. %%%---------------------------------------------------------------------- @@ -372,8 +372,9 @@ handle_info({send_element, El}, StateName, StateData) -> component => component_host(StateData), exml_packet => El}), send_element(StateData, El), {next_state, StateName, StateData}; -handle_info({route, From, To, Acc}, StateName, StateData) -> - Packet = mongoose_acc:element(Acc), +handle_info({route, Acc}, StateName, StateData) -> + {From, To, Packet} = mongoose_acc:packet(Acc), + ?LOG_DEBUG(#{what => comp_route, text => <<"Route packet to an external component">>, component => component_host(StateData), acc => Acc}), diff --git a/src/ejabberd_sm.erl b/src/ejabberd_sm.erl index 7f748b532de..cdf767e34dc 100644 --- a/src/ejabberd_sm.erl +++ b/src/ejabberd_sm.erl @@ -35,7 +35,7 @@ route/3, route/4, make_new_sid/0, - open_session/4, open_session/5, + open_session/5, close_session/4, store_info/3, get_info/2, @@ -43,7 +43,6 @@ get_user_resources/1, set_presence/6, unset_presence/5, - close_session_unset_presence/5, get_unique_sessions_number/0, get_total_sessions_number/0, get_node_sessions_number/0, @@ -114,7 +113,8 @@ priority/0, backend/0, close_reason/0, - info/0 + info/0, + info_key/0 ]). %% default value for the maximum number of user connections @@ -203,15 +203,6 @@ route(From, To, Acc, El) -> make_new_sid() -> {erlang:system_time(microsecond), self()}. --spec open_session(HostType, SID, JID, Info) -> ReplacedPids when - HostType :: binary(), - SID :: 'undefined' | sid(), - JID :: jid:jid(), - Info :: info(), - ReplacedPids :: [pid()]. -open_session(HostType, SID, JID, Info) -> - open_session(HostType, SID, JID, undefined, Info). - -spec open_session(HostType, SID, JID, Priority, Info) -> ReplacedPids when HostType :: binary(), SID :: 'undefined' | sid(), @@ -257,7 +248,7 @@ store_info(JID, Key, Value) -> {_, Pid} -> %% Ask the process to update its record itself %% Async operation - ejabberd_c2s:store_session_info(Pid, JID, Key, Value), + mongoose_c2s:async(Pid, fun ejabberd_sm:store_info/3, [JID, Key, Value]), {ok, Key} end end. @@ -289,7 +280,7 @@ remove_info(JID, Key) -> {_, Pid} -> %% Ask the process to update its record itself %% Async operation - ejabberd_c2s:remove_session_info(Pid, JID, Key), + mongoose_c2s:async(Pid, fun ejabberd_sm:remove_info/2, [JID, Key]), ok end end. @@ -353,18 +344,6 @@ unset_presence(Acc, SID, JID, Status, Info) -> mongoose_hooks:unset_presence_hook(Acc, JID, Status). --spec close_session_unset_presence(Acc, SID, JID, Status, Reason) -> Acc1 when - Acc :: mongoose_acc:t(), - SID :: 'undefined' | sid(), - JID :: jid:jid(), - Status :: binary(), - Reason :: close_reason(), - Acc1 :: mongoose_acc:t(). -close_session_unset_presence(Acc, SID, JID, Status, Reason) -> - Acc1 = close_session(Acc, SID, JID, Reason), - mongoose_hooks:unset_presence_hook(Acc1, JID, Status). - - -spec get_session_pid(JID) -> none | pid() when JID :: jid:jid(). get_session_pid(JID) -> @@ -690,7 +669,7 @@ do_route(Acc, From, To, El) -> Pid when is_pid(Pid) -> ?LOG_DEBUG(#{what => sm_route_to_pid, session_pid => Pid, acc => Acc}), - Pid ! {route, From, To, Acc}, + Pid ! {route, Acc}, Acc end end. @@ -831,7 +810,7 @@ route_message(From, To, Acc, Packet) -> %% positive fun({Prio, Pid}) when Prio == Priority -> %% we will lose message if PID is not alive - Pid ! {route, From, To, Acc}; + Pid ! {route, Acc}; %% Ignore other priority: ({_Prio, _Pid}) -> ok @@ -928,7 +907,7 @@ check_for_sessions_to_replace(HostType, JID) -> %% replacement for max_sessions. We need to check this at some point. ReplacedRedundantSessions = check_existing_resources(HostType, LUser, LServer, LResource), AllReplacedSessionPids = check_max_sessions(HostType, LUser, LServer, ReplacedRedundantSessions), - [Pid ! replaced || Pid <- AllReplacedSessionPids], + [mongoose_c2s:exit(Pid, <<"Replaced by new connection">>) || Pid <- AllReplacedSessionPids], AllReplacedSessionPids. -spec check_existing_resources(HostType, LUser, LServer, LResource) -> diff --git a/src/ejabberd_socket.erl b/src/ejabberd_socket.erl index 5828041482b..9a85923ada3 100644 --- a/src/ejabberd_socket.erl +++ b/src/ejabberd_socket.erl @@ -44,8 +44,7 @@ close/1, sockname/1, peername/1, - get_socket/1, - format_socket/1]). + get_socket/1]). -ignore_xref([change_shaper/2, compress/3, connect/3, get_peer_certificate/1, get_sockmod/1, sockname/1]). @@ -304,19 +303,3 @@ peername(#socket_state{sockmod = SockMod, socket = Socket}) -> -spec get_socket(socket_state()) -> term(). get_socket(#socket_state{socket = Socket}) -> Socket. - - -format_socket(#socket_state{sockmod = Mod, socket = Socket, - receiver = Receiver, connection_details = Info}) -> - Info2 = format_details(Info), - Info2#{socket_module => Mod, - socket => format_term(Socket), - receiver => format_term(Receiver)}; -format_socket(_) -> - #{}. - -format_term(X) -> iolist_to_binary(io_lib:format("~0p", [X])). - -format_details(Info = #{dest_address := DestAddr, src_address := SrcAddr}) -> - Info#{dest_address => inet:ntoa(DestAddr), - src_address => inet:ntoa(SrcAddr)}. diff --git a/src/ejabberd_sup.erl b/src/ejabberd_sup.erl index 302b410b01c..4e298da8382 100644 --- a/src/ejabberd_sup.erl +++ b/src/ejabberd_sup.erl @@ -88,8 +88,8 @@ init([]) -> supervisor, [ejabberd_tmp_sup]}, C2SSupervisor = - {ejabberd_c2s_sup, - {ejabberd_tmp_sup, start_link, [ejabberd_c2s_sup, ejabberd_c2s]}, + {mongoose_c2s_sup, + {ejabberd_tmp_sup, start_link, [mongoose_c2s_sup, mongoose_c2s]}, permanent, infinity, supervisor, diff --git a/src/ejabberd_zlib.erl b/src/ejabberd_zlib.erl index f7c6bba6e00..7f74d3e08c1 100644 --- a/src/ejabberd_zlib.erl +++ b/src/ejabberd_zlib.erl @@ -36,7 +36,7 @@ close/1]). -ignore_xref([close/1, controlling_process/2, disable_zlib/1, peername/1, send/2, - setopts/2, sockname/1]). + setopts/2, sockname/1, get_sockmod/1]). -define(DEFLATE, 1). -define(INFLATE, 2). diff --git a/src/jlib.erl b/src/jlib.erl index f25305c9686..b336dd10cb2 100644 --- a/src/jlib.erl +++ b/src/jlib.erl @@ -56,6 +56,8 @@ maybe_append_delay/4, remove_delay_tags/1]). +-ignore_xref([make_result_iq_reply/1]). + -include_lib("exml/include/exml.hrl"). -include_lib("exml/include/exml_stream.hrl"). % only used to define stream types -include("jlib.hrl"). diff --git a/src/logger/mongoose_log_filter.erl b/src/logger/mongoose_log_filter.erl index a8da3500f1a..dff951b7564 100644 --- a/src/logger/mongoose_log_filter.erl +++ b/src/logger/mongoose_log_filter.erl @@ -15,7 +15,6 @@ -include("mongoose.hrl"). -include_lib("jid/include/jid.hrl"). --include("ejabberd_c2s.hrl"). %% The templater in flatlog works with meta fields. %% So, we would need a filter, that takes the interesting fields @@ -104,16 +103,16 @@ remove_fields_filter(Event, _) -> Event. -c2s_state_to_map(#state{socket = Socket, streamid = StreamId, - jid = Jid, sid = Sid}) -> - SocketMap = ejabberd_socket:format_socket(Socket), +c2s_state_to_map(State) -> + SocketMap = format_socket(mongoose_c2s:get_socket(State)), + Jid = mongoose_c2s:get_jid(State), SocketMap#{ - streamid => StreamId, + streamid => mongoose_c2s:get_stream_id(State), jid => maybe_jid_to_binary(Jid), user => maybe_jid_to_luser(Jid), server => maybe_jid_to_lserver(Jid), resource => maybe_jid_to_lresource(Jid), - session_started => maybe_sid_to_timestamp(Sid)}. + session_started => maybe_sid_to_timestamp(mongoose_c2s:get_sid(State))}. format_term(X) -> iolist_to_binary(io_lib:format("~0p", [X])). @@ -129,14 +128,29 @@ maybe_jid_to_lserver(_) -> undefined. maybe_jid_to_lresource(#jid{lresource = LResource}) -> LResource; maybe_jid_to_lresource(_) -> undefined. -maybe_sid_to_timestamp({Timestamp, _Pid}) -> format_microseconds(Timestamp); -maybe_sid_to_timestamp(_) -> undefined. +maybe_sid_to_timestamp({Timestamp, _Pid}) -> format_microseconds(Timestamp). format_microseconds(N) -> calendar:system_time_to_rfc3339(N, [{unit, microsecond}, {offset, 0}, {time_designator, $T}]). +format_socket(undefined) -> + #{}; +format_socket(Socket) -> + DestAddress = mongoose_c2s_socket:get_ip(Socket), + #{ + transport => mongoose_c2s_socket:get_transport(Socket), + conn_type => mongoose_c2s_socket:get_conn_type(Socket), + dest_address => format_address(DestAddress) + }. + +format_address({Address, Port}) -> + #{ + address => inet:ntoa(Address), + port => Port + }. + format_stacktrace_args([{_Mod,_Fun,Args,_Info}|_]) when is_list(Args) -> iolist_to_binary(io_lib:format("~p", [Args])); format_stacktrace_args(_) -> diff --git a/src/mod_bosh_socket.erl b/src/mod_bosh_socket.erl index a30756eff24..eb856157a94 100644 --- a/src/mod_bosh_socket.erl +++ b/src/mod_bosh_socket.erl @@ -2,6 +2,7 @@ -behaviour(gen_fsm_compat). -behaviour(mongoose_transport). +-behaviour(mongoose_c2s_socket). %% API -export([start/4, @@ -30,6 +31,18 @@ peername/1, get_peer_certificate/1]). +%% mongoose_c2s_socket callbacks +-export([socket_new/2, + socket_peername/1, + tcp_to_tls/2, + socket_handle_data/2, + socket_activate/1, + socket_send_xml/2, + socket_close/1, + has_peer_cert/2, + is_channel_binding_supported/1, + is_ssl/1]). + %% gen_fsm callbacks -export([init/1, accumulate/2, accumulate/3, @@ -192,11 +205,17 @@ get_cached_responses(Pid) -> %%-------------------------------------------------------------------- init([HostType, Sid, Peer, PeerCert]) -> BoshSocket = #bosh_socket{sid = Sid, pid = self(), peer = Peer, peercert = PeerCert}, - C2SOpts = #{access => all, shaper => none, xml_socket => true, hibernate_after => 0}, - {ok, C2SPid} = ejabberd_c2s:start({mod_bosh_socket, BoshSocket}, C2SOpts), + C2SOpts = #{access => all, + shaper => none, + xml_socket => true, + max_stanza_size => 0, + hibernate_after => 0, + c2s_state_timeout => 5000, + backwards_compatible_session => true}, + {ok, C2SPid} = mongoose_c2s:start({?MODULE, BoshSocket, C2SOpts}, []), Opts = gen_mod:get_loaded_module_opts(HostType, mod_bosh), State = new_state(Sid, C2SPid, Opts), - ?LOG_DEBUG(ls(#{what => bosh_socket_init}, State)), + ?LOG_DEBUG(ls(#{what => bosh_socket_init, peer => Peer}, State)), {ok, accumulate, State}. new_state(Sid, C2SPid, #{inactivity := Inactivity, max_wait := MaxWait, @@ -416,7 +435,7 @@ handle_info(Info, SName, State) -> terminate(Reason, StateName, #state{sid = Sid, handlers = Handlers} = S) -> [Pid ! {close, Sid} || {_, _, Pid} <- lists:sort(Handlers)], mod_bosh_backend:delete_session(Sid), - catch ejabberd_c2s:stop(S#state.c2s_pid), + catch mongoose_c2s:stop(S#state.c2s_pid, normal), ?LOG_DEBUG(ls(#{what => bosh_socket_closing_session, reason => Reason, state_name => StateName, handlers => Handlers, pending => S#state.pending}, S)). @@ -787,9 +806,10 @@ store(Data, #state{pending = Pending} = S) -> S#state{pending = Pending ++ Data}. --spec forward_to_c2s('undefined' | pid(), jlib:xmlstreamel()) -> 'ok'. +-spec forward_to_c2s(pid() | undefined, jlib:xmlstreamel()) -> ok. forward_to_c2s(C2SPid, StreamElement) -> - gen_fsm_compat:send_event(C2SPid, StreamElement). + C2SPid ! {tcp, undefined, StreamElement}, + ok. -spec maybe_add_handler(_, rid(), state()) -> state(). @@ -854,10 +874,10 @@ bosh_unwrap(streamend, Body, State) -> bosh_unwrap(normal, Body, #state{sid = Sid} = State) -> Sid = exml_query:attr(Body, <<"sid">>), ?NS_HTTPBIND = exml_query:attr(Body, <<"xmlns">>), - {[{xmlstreamelement, El} - || El <- Body#xmlel.children, - %% Ignore whitespace keepalives. - El /= #xmlcdata{content = <<" ">>}], + + {[El || El <- Body#xmlel.children, + %% Ignore whitespace keepalives. + El /= #xmlcdata{content = <<" ">>}], State}. @@ -1104,6 +1124,58 @@ ls(LogMap, State) -> ignore_undefined(Map) -> maps:filter(fun(_, V) -> V =/= undefined end, Map). +%% mongoose_c2s_socket callbacks + +-spec socket_new(mod_bosh:socket(), mongoose_listener:options()) -> mod_bosh:socket(). +socket_new(Socket, _LOpts) -> + Socket. + +-spec socket_peername(mod_bosh:socket()) -> {inet:ip_address(), inet:port_number()}. +socket_peername(Socket) -> + {ok, Peername} = peername(Socket), + Peername. + +-spec tcp_to_tls(mod_bosh:socket(), mongoose_listener:options()) -> + {ok, mod_bosh:socket()} | {error, term()}. +tcp_to_tls(_Socket, _LOpts) -> + {error, tls_not_allowed_on_bosh}. + +-spec socket_handle_data(mod_bosh:socket(), {tcp | ssl, term(), iodata()}) -> + iodata() | {raw, [exml:element()]} | {error, term()}. +socket_handle_data(_Socket, {_Kind, _Term, Packet}) -> + {raw, [Packet]}. + +-spec socket_activate(mod_bosh:socket()) -> ok. +socket_activate(_Socket) -> + ok. + +-spec socket_send_xml(mod_bosh:socket(), + iodata() | exml_stream:element() | [exml_stream:element()]) -> + ok | {error, term()}. +socket_send_xml(#bosh_socket{pid = Pid}, XMLs) when is_list(XMLs) -> + [Pid ! {send, XML} || XML <- XMLs], + ok; +socket_send_xml(#bosh_socket{pid = Pid}, XML) -> + Pid ! {send, XML}, + ok. + +-spec socket_close(mod_bosh:socket()) -> ok. +socket_close(Socket) -> + close(Socket), + ok. + +-spec has_peer_cert(mod_bosh:socket(), mongoose_listener:options()) -> boolean(). +has_peer_cert(Socket, _LOpts) -> + get_peer_certificate(Socket) /= no_peer_cert. + +-spec is_channel_binding_supported(mod_bosh:socket()) -> boolean(). +is_channel_binding_supported(_Socket) -> + false. + +-spec is_ssl(mod_bosh:socket()) -> boolean(). +is_ssl(_Socket) -> + false. + %%-------------------------------------------------------------------- %% Tests %%-------------------------------------------------------------------- diff --git a/src/mod_caps.erl b/src/mod_caps.erl index 7118f6c3363..08d23951fba 100644 --- a/src/mod_caps.erl +++ b/src/mod_caps.erl @@ -46,13 +46,13 @@ handle_cast/2, terminate/2, code_change/3]). -export([user_send_packet/4, user_receive_packet/5, - c2s_presence_in/4, c2s_filter_packet/5, + user_receive_presence/3, c2s_filter_packet/5, c2s_broadcast_recipients/5]). %% for test cases -export([delete_caps/1, make_disco_hash/2]). --ignore_xref([c2s_broadcast_recipients/5, c2s_filter_packet/5, c2s_presence_in/4, +-ignore_xref([c2s_broadcast_recipients/5, c2s_filter_packet/5, caps_stream_features/3, delete_caps/1, disco_info/1, disco_local_identity/1, make_disco_hash/2, read_caps/1, start_link/2, user_receive_packet/5, user_send_packet/4]). @@ -249,39 +249,59 @@ disco_info(Acc = #{node := Node}) -> false -> Acc end. --spec c2s_presence_in(ejabberd_c2s:state(), jid:jid(), jid:jid(), exml:element()) -> - ejabberd_c2s:state(). -c2s_presence_in(C2SState, From, To, Packet = #xmlel{attrs = Attrs, children = Els}) -> - ?LOG_DEBUG(#{what => caps_c2s_presence_in, +-spec user_receive_presence(mongoose_acc:t(), mongoose_c2s_hooks:hook_params(), map()) -> + mongoose_c2s_hooks:hook_result(). +user_receive_presence(Acc, #{c2s_data := StateData}, _Extra) -> + {From, To, #xmlel{attrs = Attrs, children = Els} = Packet} = mongoose_acc:packet(Acc), + ?LOG_DEBUG(#{what => user_receive_presence, to => jid:to_binary(To), from => jid:to_binary(From), - exml_packet => Packet, c2s_state => C2SState}), + exml_packet => Packet, c2s_state => StateData}), Type = xml:get_attr_s(<<"type">>, Attrs), - Subscription = ejabberd_c2s:get_subscription(From, C2SState), - Insert = (Type == <<>> orelse Type == <<"available">>) - and (Subscription == both orelse Subscription == to), - Delete = Type == <<"unavailable">> orelse Type == <<"error">>, - case Insert orelse Delete of - true -> - LFrom = jid:to_lower(From), - Rs = case ejabberd_c2s:get_aux_field(caps_resources, - C2SState) - of - {ok, Rs1} -> Rs1; - error -> gb_trees:empty() - end, - Caps = read_caps(Els), - NewRs = case Caps of - nothing when Insert == true -> Rs; - _ when Insert == true -> - ?LOG_DEBUG(#{what => caps_set_caps, caps => Caps, - to => jid:to_binary(To), from => jid:to_binary(From), - exml_packet => Packet, c2s_state => C2SState}), - upsert_caps(LFrom, Caps, Rs); - _ -> gb_trees:delete_any(LFrom, Rs) - end, - ejabberd_c2s:set_aux_field(caps_resources, NewRs, - C2SState); - false -> C2SState + case mongoose_c2s:get_mod_state(StateData, mod_presence) of + {ok, Presences} -> + Subscription = get_subscription(From, Presences), + Insert = (Type == <<>> orelse Type == <<"available">>) + and (Subscription == both orelse Subscription == to), + Delete = Type == <<"unavailable">> orelse Type == <<"error">>, + case Insert orelse Delete of + true -> + LFrom = jid:to_lower(From), + Rs = case mongoose_c2s:get_mod_state(StateData, ?MODULE) of + {ok, Rs1} -> Rs1; + {error, not_found} -> gb_trees:empty() + end, + Caps = read_caps(Els), + NewRs = case Caps of + nothing when Insert == true -> + Rs; + _ when Insert == true -> + ?LOG_DEBUG(#{what => caps_set_caps, + caps => Caps, + to => jid:to_binary(To), + from => jid:to_binary(From), + exml_packet => Packet, + c2s_state => StateData}), + upsert_caps(LFrom, Caps, Rs); + _ -> + gb_trees:delete_any(LFrom, Rs) + end, + {ok, mongoose_c2s_acc:to_acc(Acc, state_mod, {?MODULE, NewRs})}; + false -> + {ok, Acc} + end; + {error, not_found} -> + {ok, Acc} + end. + +get_subscription(From, Presences) -> + BareFrom = jid:to_bare(From), + F = mod_presence:is_subscribed_to_my_presence(From, BareFrom, Presences), + T = mod_presence:am_i_subscribed_to_presence(From, BareFrom, Presences), + case {F, T} of + {true, true} -> both; + {true, false} -> from; + {false, true} -> to; + {false, false} -> none end. -spec upsert_caps(jid:simple_jid(), caps(), caps_resources()) -> caps_resources(). @@ -394,7 +414,8 @@ legacy_hooks(HostType) -> {disco_info, HostType, ?MODULE, disco_info, 1}]. hooks(HostType) -> - [{disco_local_features, HostType, fun ?MODULE:disco_local_features/3, #{}, 1}]. + [{disco_local_features, HostType, fun ?MODULE:disco_local_features/3, #{}, 1}, + {user_receive_presence, HostType, fun ?MODULE:user_receive_presence/3, #{}, 1}]. -spec code_change(any(), state(), any()) -> {ok, state()}. code_change(_OldVsn, State, _Extra) -> {ok, State}. diff --git a/src/mod_ping.erl b/src/mod_ping.erl index efb56db189e..18f3fb529c6 100644 --- a/src/mod_ping.erl +++ b/src/mod_ping.erl @@ -128,7 +128,7 @@ iq_ping(Acc, _From, _To, #iq{sub_el = SubEl} = IQ, _) -> user_send_iq(Acc, #{c2s_data := StateData}, #{host_type := HostType}) -> case {mongoose_acc:stanza_type(Acc), mongoose_c2s:get_mod_state(StateData, ?MODULE)} of - {<<"result">>, #ping_handler{id = PingId, time = T0}} -> + {<<"result">>, {ok, #ping_handler{id = PingId, time = T0}}} -> IqResponse = mongoose_acc:element(Acc), IqId = exml_query:attr(IqResponse, <<"id">>), case {IqId, PingId} of diff --git a/src/mod_presence.erl b/src/mod_presence.erl index 04d6e294888..48e67f40306 100644 --- a/src/mod_presence.erl +++ b/src/mod_presence.erl @@ -42,9 +42,11 @@ -export([ get/2, is_subscribed_to_my_presence/3, + am_i_subscribed_to_presence/3, presence_unavailable_stanza/0, get_presence/1, - set_presence/2 + set_presence/2, + maybe_get_handler/1 ]). -spec start(mongooseim:host_type(), gen_mod:module_opts()) -> ok. @@ -603,13 +605,16 @@ close_session_status(_) -> -spec maybe_get_handler(mongoose_c2s:c2s_data()) -> presences_state(). maybe_get_handler(StateData) -> case mongoose_c2s:get_mod_state(StateData, ?MODULE) of - #presences_state{} = Presences -> Presences; + {ok, #presences_state{} = Presences} -> Presences; {error, not_found} -> #presences_state{} end. -spec get_mod_state(mongoose_c2s:c2s_data()) -> presences_state() | {error, not_found}. get_mod_state(StateData) -> - mongoose_c2s:get_mod_state(StateData, ?MODULE). + case mongoose_c2s:get_mod_state(StateData, ?MODULE) of + {ok, Presence} -> Presence; + Error -> Error + end. -spec get_priority_from_presence(exml:element()) -> priority(). get_priority_from_presence(PresencePacket) -> @@ -635,6 +640,11 @@ update_priority(Acc, Priority, Packet, StateData) -> Jid = mongoose_c2s:get_jid(StateData), ejabberd_sm:set_presence(Acc, Sid, Jid, Priority, Packet, #{}). +am_i_subscribed_to_presence(LJID, LBareJID, S) -> + gb_sets:is_element(LJID, S#presences_state.pres_t) + orelse (LJID /= LBareJID) + andalso gb_sets:is_element(LBareJID, S#presences_state.pres_t). + -spec am_i_available_to(jid:jid(), jid:jid(), presences_state()) -> boolean(). am_i_available_to(FromJid, BareJid, Presences) -> gb_sets:is_element(FromJid, Presences#presences_state.pres_a) diff --git a/src/mod_websockets.erl b/src/mod_websockets.erl index 90ce13085f0..a1f4dfb77cf 100644 --- a/src/mod_websockets.erl +++ b/src/mod_websockets.erl @@ -8,6 +8,7 @@ -behaviour(mongoose_http_handler). -behaviour(cowboy_websocket). -behaviour(mongoose_transport). +-behaviour(mongoose_c2s_socket). %% mongoose_http_handler callbacks -export([config_spec/0]). @@ -19,6 +20,18 @@ websocket_info/2, terminate/3]). +%% mongoose_c2s_socket callbacks +-export([socket_new/2, + socket_peername/1, + tcp_to_tls/2, + socket_handle_data/2, + socket_activate/1, + socket_close/1, + socket_send_xml/2, + has_peer_cert/2, + is_channel_binding_supported/1, + is_ssl/1]). + %% ejabberd_socket compatibility -export([starttls/2, starttls/3, compress/1, compress/3, @@ -52,6 +65,7 @@ -record(ws_state, { peer :: mongoose_transport:peer() | undefined, fsm_pid :: pid() | undefined, + fsm_module :: module() | undefined, open_tag :: stream | open | undefined, parser :: exml_stream:parser() | undefined, opts :: map(), @@ -73,9 +87,14 @@ config_spec() -> validate = positive}, <<"max_stanza_size">> => #option{type = int_or_infinity, validate = positive}, - <<"service">> => mongoose_config_spec:xmpp_listener_extra(service)}, + <<"service">> => mongoose_config_spec:xmpp_listener_extra(service), + <<"c2s_state_timeout">> => #option{type = int_or_infinity, + validate = non_negative}, + <<"backwards_compatible_session">> => #option{type = boolean}}, defaults = #{<<"timeout">> => 60000, - <<"max_stanza_size">> => infinity} + <<"max_stanza_size">> => infinity, + <<"c2s_state_timeout">> => 5000, + <<"backwards_compatible_session">> => true} }. %%-------------------------------------------------------------------- @@ -190,21 +209,24 @@ handle_text(Text, #ws_state{parser = Parser} = State) -> process_parse_error(Reason, State) end. -process_client_elements(Elements, #ws_state{fsm_pid = FSM} = State) -> +process_client_elements(Elements, #ws_state{fsm_pid = FSM, fsm_module = FSMModule} = State) -> {Elements1, State1} = process_client_stream_start(Elements, State), - [send_to_fsm(FSM, process_client_stream_end( + [send_to_fsm(FSMModule, FSM, process_client_stream_end( replace_stream_ns(Elem, State1), State1)) || Elem <- Elements1], {ok, State1}. process_parse_error(_Reason, #ws_state{fsm_pid = undefined} = State) -> {stop, State}; -process_parse_error(Reason, #ws_state{fsm_pid = FSM} = State) -> - send_to_fsm(FSM, {xmlstreamerror, Reason}), +process_parse_error(Reason, #ws_state{fsm_pid = FSM, fsm_module = FSMModule} = State) -> + send_to_fsm(FSMModule, FSM, {xmlstreamerror, Reason}), {ok, State}. -send_to_fsm(FSM, #xmlel{} = Element) -> - send_to_fsm(FSM, {xmlstreamelement, Element}); -send_to_fsm(FSM, StreamElement) -> +send_to_fsm(mongoose_c2s, FSM, Element) -> + FSM ! {tcp, undefined, Element}, + ok; +send_to_fsm(ejabberd_service, FSM, #xmlel{} = Element) -> + send_to_fsm(ejabberd_service, FSM, {xmlstreamelement, Element}); +send_to_fsm(ejabberd_service, FSM, StreamElement) -> p1_fsm:send_event(FSM, StreamElement). maybe_start_fsm([#xmlstreamstart{ name = <<"stream", _/binary>>, attrs = Attrs} @@ -217,9 +239,18 @@ maybe_start_fsm([#xmlstreamstart{ name = <<"stream", _/binary>>, attrs = Attrs} {stop, State} end; maybe_start_fsm([#xmlel{ name = <<"open">> }], - #ws_state{fsm_pid = undefined} = State) -> - Opts = #{access => all, shaper => none, xml_socket => true, hibernate_after => 0}, - do_start_fsm(ejabberd_c2s, Opts, State); + #ws_state{fsm_pid = undefined, + opts = #{c2s_state_timeout := StateTimeout, + backwards_compatible_session := BackwardsCompatible}} = State) -> + Opts = #{ + access => all, + shaper => none, + max_stanza_size => 0, + xml_socket => true, + hibernate_after => 0, + c2s_state_timeout => StateTimeout, + backwards_compatible_session => BackwardsCompatible}, + do_start_fsm(mongoose_c2s, Opts, State); maybe_start_fsm(_Els, State) -> {ok, State}. @@ -233,7 +264,7 @@ do_start_fsm(FSMModule, Opts, State = #ws_state{peer = Peer, peercert = PeerCert text => <<"WebSockets starts c2s process">>, c2s_pid => Pid, c2s_module => FSMModule, peer => State#ws_state.peer}), - NewState = State#ws_state{fsm_pid = Pid, peercert = passed}, + NewState = State#ws_state{fsm_pid = Pid, fsm_module = FSMModule, peercert = passed}, {ok, NewState}; {error, Reason} -> ?LOG_WARNING(#{what => ws_c2s_start_failed, @@ -243,8 +274,9 @@ do_start_fsm(FSMModule, Opts, State = #ws_state{peer = Peer, peercert = PeerCert {stop, State#ws_state{peercert = passed}} end. -call_fsm_start(ejabberd_c2s, SocketData, Opts) -> - ejabberd_c2s:start({?MODULE, SocketData}, Opts); +call_fsm_start(mongoose_c2s, SocketData, #{hibernate_after := HibernateAfterTimeout} = Opts) -> + mongoose_c2s:start({?MODULE, SocketData, Opts}, + [{hibernate_after, HibernateAfterTimeout}]); call_fsm_start(ejabberd_service, SocketData, Opts) -> ejabberd_service:start({?MODULE, SocketData}, Opts). @@ -423,3 +455,54 @@ case_insensitive_match(LowerPattern, [Case | Cases]) -> end; case_insensitive_match(_, []) -> nomatch. + +%% mongoose_c2s_socket callbacks + +-spec socket_new(socket(), mongoose_listener:options()) -> socket(). +socket_new(Socket, _LOpts) -> + Socket. + +-spec socket_peername(socket()) -> {inet:ip_address(), inet:port_number()}. +socket_peername(Socket) -> + {ok, Peername} = peername(Socket), + Peername. + +-spec tcp_to_tls(socket(), mongoose_listener:options()) -> + {ok, socket()} | {error, term()}. +tcp_to_tls(_Socket, _LOpts) -> + {error, tls_not_allowed_on_websockets}. + +-spec socket_handle_data(socket(), {tcp | ssl, term(), term()}) -> + iodata() | {raw, [exml:element()]} | {error, term()}. +socket_handle_data(_Socket, {_Kind, _Term, Packet}) -> + {raw, [Packet]}. + +-spec socket_activate(socket()) -> ok. +socket_activate(_Socket) -> + ok. + +-spec socket_close(socket()) -> ok. +socket_close(Socket) -> + close(Socket), + ok. + +-spec socket_send_xml(socket(), iodata() | exml:element() | [exml:element()]) -> + ok | {error, term()}. +socket_send_xml(#websocket{pid = Pid}, XMLs) when is_list(XMLs) -> + [Pid ! {send_xml, XML} || XML <- XMLs], + ok; +socket_send_xml(#websocket{pid = Pid}, XML) -> + Pid ! {send_xml, XML}, + ok. + +-spec has_peer_cert(socket(), mongoose_listener:options()) -> boolean(). +has_peer_cert(Socket, _LOpts) -> + get_peer_certificate(Socket) /= no_peer_cert. + +-spec is_channel_binding_supported(socket()) -> boolean(). +is_channel_binding_supported(_Socket) -> + false. + +-spec is_ssl(socket()) -> boolean(). +is_ssl(_Socket) -> + false. diff --git a/src/mongoose_client_api/mongoose_client_api_sse.erl b/src/mongoose_client_api/mongoose_client_api_sse.erl index 18b3e88df4e..fe85c2ef3b8 100644 --- a/src/mongoose_client_api/mongoose_client_api_sse.erl +++ b/src/mongoose_client_api/mongoose_client_api_sse.erl @@ -42,7 +42,7 @@ handle_notify(Msg, State) -> ?UNEXPECTED_INFO(Msg), {nosend, State}. -handle_info({route, _From, _To, Acc}, State) -> +handle_info({route, Acc}, State) -> #xmlel{ name = TagName } = El = mongoose_acc:element(Acc), handle_msg(TagName, Acc, El, State); handle_info(Msg, State) -> diff --git a/src/mongoose_hooks.erl b/src/mongoose_hooks.erl index 1a83f2acb1b..9e9b4516023 100644 --- a/src/mongoose_hooks.erl +++ b/src/mongoose_hooks.erl @@ -36,20 +36,13 @@ unregister_subhost/1, user_available_hook/2, user_ping_response/5, - user_receive_packet/6, - user_sent_keep_alive/2, user_send_packet/4, vcard_set/4, xmpp_send_element/3, xmpp_stanza_dropped/4]). -export([c2s_broadcast_recipients/4, - c2s_filter_packet/4, - c2s_preprocessing_hook/3, - c2s_presence_in/4, c2s_stream_features/2, - c2s_unauthenticated_iq/4, - c2s_update_presence/2, check_bl_c2s/1, forbidden_session_hook/3, session_opening_allowed_for_user/2]). @@ -153,8 +146,6 @@ -export([mod_global_distrib_known_recipient/4, mod_global_distrib_unknown_recipient/2]). --export([c2s_remote_hook/5]). - -export([remove_domain/2, node_cleanup/1]). @@ -172,17 +163,6 @@ Packet :: exml:element()}. -export_type([filter_packet_acc/0]). --spec c2s_remote_hook(HostType, Tag, Args, HandlerState, C2SState) -> Result when - HostType :: binary(), - Tag :: atom(), - Args :: term(), - HandlerState :: term(), - C2SState :: ejabberd_c2s:state(), - Result :: term(). % ok | empty_state | HandlerState -c2s_remote_hook(HostType, Tag, Args, HandlerState, C2SState) -> - run_hook_for_host_type(c2s_remote_hook, HostType, HandlerState, - [Tag, Args, C2SState]). - -spec adhoc_local_commands(HostType, From, To, AdhocRequest) -> Result when HostType :: mongooseim:host_type(), From :: jid:jid(), @@ -463,24 +443,6 @@ user_ping_response(HostType, Acc, JID, Response, TDelta) -> ParamsWithLegacyArgs = ejabberd_hooks:add_args(Params, Args), run_hook_for_host_type(user_ping_response, HostType, Acc, ParamsWithLegacyArgs). --spec user_receive_packet(HostType, Acc, JID, From, To, El) -> Result when - HostType :: binary(), - Acc :: mongoose_acc:t(), - JID :: jid:jid(), - From :: jid:jid(), - To :: jid:jid(), - El :: exml:element(), - Result :: mongoose_acc:t(). -user_receive_packet(HostType, Acc, JID, From, To, El) -> - run_hook_for_host_type(user_receive_packet, HostType, Acc, [JID, From, To, El]). - --spec user_sent_keep_alive(HostType, JID) -> Result when - HostType :: binary(), - JID :: jid:jid(), - Result :: any(). -user_sent_keep_alive(HostType, JID) -> - run_hook_for_host_type(user_sent_keep_alive, HostType, ok, [JID]). - %%% @doc A hook called when a user sends an XMPP stanza. %%% The hook's handler is expected to accept four parameters: %%% `Acc', `From', `To' and `Packet' @@ -532,45 +494,16 @@ xmpp_stanza_dropped(Acc, From, To, Packet) -> %% C2S related hooks -spec c2s_broadcast_recipients(State, Type, From, Packet) -> Result when - State :: ejabberd_c2s:state(), + State :: mongoose_c2s:state(), Type :: {atom(), any()}, From :: jid:jid(), Packet :: exml:element(), Result :: [jid:simple_jid()]. c2s_broadcast_recipients(State, Type, From, Packet) -> - HostType = ejabberd_c2s_state:host_type(State), + HostType = mongoose_c2s:get_host_type(State), run_hook_for_host_type(c2s_broadcast_recipients, HostType, [], [State, Type, From, Packet]). --spec c2s_filter_packet(State, Feature, To, Packet) -> Result when - State :: ejabberd_c2s:state(), - Feature :: {atom(), binary()}, - To :: jid:jid(), - Packet :: exml:element(), - Result :: boolean(). -c2s_filter_packet(State, Feature, To, Packet) -> - HostType = ejabberd_c2s_state:host_type(State), - run_hook_for_host_type(c2s_filter_packet, HostType, true, - [State, Feature, To, Packet]). - --spec c2s_preprocessing_hook(HostType, Acc, State) -> Result when - HostType :: mongooseim:host_type(), - Acc :: mongoose_acc:t(), - State :: mongoose_c2s:c2s_data(), - Result :: mongoose_acc:t(). -c2s_preprocessing_hook(HostType, Acc, State) -> - run_hook_for_host_type(c2s_preprocessing_hook, HostType, Acc, [State]). - --spec c2s_presence_in(State, From, To, Packet) -> Result when - State :: ejabberd_c2s:state(), - From :: jid:jid(), - To :: jid:jid(), - Packet :: exml:element(), - Result :: ejabberd_c2s:state(). -c2s_presence_in(State, From, To, Packet) -> - HostType = ejabberd_c2s_state:host_type(State), - run_hook_for_host_type(c2s_presence_in, HostType, State, [From, To, Packet]). - -spec c2s_stream_features(HostType, LServer) -> Result when HostType :: mongooseim:host_type(), LServer :: jid:lserver(), @@ -578,22 +511,6 @@ c2s_presence_in(State, From, To, Packet) -> c2s_stream_features(HostType, LServer) -> run_hook_for_host_type(c2s_stream_features, HostType, [], [HostType, LServer]). --spec c2s_unauthenticated_iq(HostType, Server, IQ, IP) -> Result when - HostType :: mongooseim:host_type(), - Server :: jid:server(), - IQ :: jlib:iq(), - IP :: {inet:ip_address(), inet:port_number()} | undefined, - Result :: exml:element() | empty. -c2s_unauthenticated_iq(HostType, Server, IQ, IP) -> - run_hook_for_host_type(c2s_unauthenticated_iq, HostType, empty, [HostType, Server, IQ, IP]). - --spec c2s_update_presence(HostType, Acc) -> Result when - HostType :: mongooseim:host_type(), - Acc :: mongoose_acc:t(), - Result :: mongoose_acc:t(). -c2s_update_presence(HostType, Acc) -> - run_hook_for_host_type(c2s_update_presence, HostType, Acc, []). - -spec check_bl_c2s(IP) -> Result when IP :: inet:ip_address(), Result :: boolean(). diff --git a/src/mongoose_iq.erl b/src/mongoose_iq.erl index ccad77e3ebb..69bfae5e219 100644 --- a/src/mongoose_iq.erl +++ b/src/mongoose_iq.erl @@ -23,6 +23,7 @@ -export([info/1, xmlns/1, command/1]). -ignore_xref([command/1, empty_result_iq/1, iq_to_sub_el/1, update_acc_info/1]). +-ignore_xref([xmlns/1]). -include("jlib.hrl"). diff --git a/src/mongoose_tls.erl b/src/mongoose_tls.erl index 7bca8f061b9..a41390bbcfd 100644 --- a/src/mongoose_tls.erl +++ b/src/mongoose_tls.erl @@ -30,6 +30,8 @@ -ignore_xref([behaviour_info/1, close/1, controlling_process/2, peername/1, send/2, setopts/2, sockname/1]). +-ignore_xref([get_sockmod/1]). + -type tls_socket() :: any(). -type cert() :: {ok, Cert::any()} | {bad_cert, bitstring()} | no_peer_cert. diff --git a/src/mongoose_transport.erl b/src/mongoose_transport.erl index a7d85636e4d..11c3e9a93ad 100644 --- a/src/mongoose_transport.erl +++ b/src/mongoose_transport.erl @@ -44,7 +44,7 @@ -export([starttls/3, starttls/4, monitor/2, send_xml/3, peername/2, get_peer_certificate/2]). --ignore_xref([behaviour_info/1]). +-ignore_xref([get_peer_certificate/2, monitor/2, peername/2, send_xml/3, starttls/3]). -spec starttls(TransportMod :: module(), Transport :: t(), mongoose_tls:options()) -> t(). starttls(TransportMod, Transport, TLSOpts) -> TransportMod:starttls(Transport, TLSOpts). diff --git a/src/privacy/mod_blocking.erl b/src/privacy/mod_blocking.erl index 7107c799b62..b5789418deb 100644 --- a/src/privacy/mod_blocking.erl +++ b/src/privacy/mod_blocking.erl @@ -111,7 +111,7 @@ blocking_push_to_resources(Action, JIDs, StateData) -> -spec blocking_presence_to_contacts(blocking_type(), [binary()], mongoose_c2s:c2s_data()) -> ok. blocking_presence_to_contacts(_Action, [], _StateData) -> ok; blocking_presence_to_contacts(Action, [Jid | JIDs], StateData) -> - Presences = mongoose_c2s:get_mod_state(StateData, mod_presence), + Presences = mod_presence:maybe_get_handler(StateData), Pres = case Action of block -> mod_presence:presence_unavailable_stanza(); diff --git a/src/privacy/mod_privacy.erl b/src/privacy/mod_privacy.erl index a3ec14aa382..b7979041d6d 100644 --- a/src/privacy/mod_privacy.erl +++ b/src/privacy/mod_privacy.erl @@ -298,7 +298,7 @@ process_privacy_iq(Acc, HostType, set, ToJid, StateData) -> maybe_update_presence(Acc, StateData, OldList, NewList) -> Jid = mongoose_c2s:get_jid(StateData), - Presences = mongoose_c2s:get_mod_state(StateData, mod_presence), + Presences = mod_presence:maybe_get_handler(StateData), FromS = mod_presence:get(Presences, s_from), % Our own jid is added to pres_f, even though we're not a "contact", so for % the purposes of this check we don't want it: @@ -361,7 +361,7 @@ send_back_error(Acc, _, _) -> get_handler(StateData) -> case mongoose_c2s:get_mod_state(StateData, ?MODULE) of {error, not_found} -> get_privacy_list(StateData); - Handler -> Handler + {ok, Handler} -> Handler end. -spec get_privacy_list(mongoose_c2s:c2s_data()) -> mongoose_privacy:userlist(). diff --git a/src/shaper.erl b/src/shaper.erl index 39238535788..41e8940be73 100644 --- a/src/shaper.erl +++ b/src/shaper.erl @@ -41,7 +41,7 @@ new(Name) -> %% @doc Update shaper. %% `Delay' is how many milliseconds to wait. --spec update(shaper(), Size :: pos_integer()) -> {shaper(), Delay :: non_neg_integer()}. +-spec update(shaper(), Size :: non_neg_integer()) -> {shaper(), Delay :: non_neg_integer()}. update(none, _Size) -> {none, 0}; update(#shaper{max_rate = MaxRatePerSecond, diff --git a/src/stream_management/mod_stream_management.erl b/src/stream_management/mod_stream_management.erl index 6d3b469708d..7925e800575 100644 --- a/src/stream_management/mod_stream_management.erl +++ b/src/stream_management/mod_stream_management.erl @@ -40,8 +40,10 @@ register_stale_smid_h/3, remove_stale_smid_h/2]). --ignore_xref([c2s_stream_features/3, get_sid/2, get_stale_h/2, - register_stale_smid_h/3, remove_stale_smid_h/2, session_cleanup/5]). +-ignore_xref([c2s_stream_features/3, get_sid/2, get_stale_h/2, remove_smid/5, + register_stale_smid_h/3, remove_stale_smid_h/2, session_cleanup/5, + get_ack_freq/1, get_buffer_max/1, get_resume_timeout/1, + get_session_from_smid/2, make_smid/0, register_smid/3]). -include("mongoose.hrl"). -include("jlib.hrl"). @@ -686,7 +688,10 @@ build_sm_handler(HostType) -> -spec get_mod_state(mongoose_c2s:c2s_data()) -> sm_state() | {error, not_found}. get_mod_state(StateData) -> - mongoose_c2s:get_mod_state(StateData, ?MODULE). + case mongoose_c2s:get_mod_state(StateData, ?MODULE) of + {ok, State} -> State; + Error -> Error + end. -spec get_peer_state(pid()) -> {ok, mongoose_c2s:c2s_data()} | {_, _, _}. get_peer_state(Pid) -> diff --git a/src/system_metrics/mongoose_system_metrics_collector.erl b/src/system_metrics/mongoose_system_metrics_collector.erl index 4cca5f7e821..f643d42139e 100644 --- a/src/system_metrics/mongoose_system_metrics_collector.erl +++ b/src/system_metrics/mongoose_system_metrics_collector.erl @@ -132,7 +132,7 @@ filter_unknown_api(ApiList) -> get_transport_mechanisms() -> HTTP = [Mod || Mod <- get_http_handler_modules(), Mod =:= mod_bosh orelse Mod =:= mod_websockets], - TCP = lists:usort([tcp || #{proto := tcp} <- get_listeners(ejabberd_c2s)]), + TCP = lists:usort([tcp || #{proto := tcp} <- get_listeners(mongoose_c2s_listener)]), [#{report_name => transport_mechanism, key => Transport, value => enabled} || Transport <- HTTP ++ TCP]. @@ -149,7 +149,7 @@ get_http_handler_modules(#{handlers := Handlers}) -> [Module || #{module := Module} <- Handlers]. get_tls_options() -> - TLSOptions = lists:flatmap(fun extract_tls_options/1, get_listeners(ejabberd_c2s)), + TLSOptions = lists:flatmap(fun extract_tls_options/1, get_listeners(mongoose_c2s_listener)), [#{report_name => tls_option, key => TLSMode, value => TLSModule} || {TLSMode, TLSModule} <- lists:usort(TLSOptions)]. diff --git a/test/common/config_parser_helper.erl b/test/common/config_parser_helper.erl index 6bfcf82dd0f..a0e8c89413a 100644 --- a/test/common/config_parser_helper.erl +++ b/test/common/config_parser_helper.erl @@ -1072,7 +1072,9 @@ default_config([listen, http]) -> default_config([listen, http, handlers, mod_websockets]) -> #{timeout => 60000, max_stanza_size => infinity, - module => mod_websockets}; + module => mod_websockets, + c2s_state_timeout => 5000, + backwards_compatible_session => true}; default_config([listen, http, handlers, mongoose_admin_api]) -> #{handlers => [contacts, users, sessions, messages, stanzas, muc_light, muc, inbox, domain, metrics], diff --git a/test/cowboy_SUITE.erl b/test/cowboy_SUITE.erl index 4500c72733d..8e0f29b07f9 100644 --- a/test/cowboy_SUITE.erl +++ b/test/cowboy_SUITE.erl @@ -18,6 +18,7 @@ -compile([export_all, nowarn_export_all]). -include_lib("common_test/include/ct.hrl"). +-include_lib("eunit/include/eunit.hrl"). -define(SERVER, "http://localhost:8080"). @@ -221,9 +222,9 @@ start_cowboy_returns_error_eaddrinuse(_C) -> handlers => [], transport => default_config([listen, http, transport]), protocol => default_config([listen, http, protocol])}, - {ok, _Pid} = ejabberd_cowboy:start_cowboy(a_ref, Opts, 2, 10), + ?assertMatch({ok, _}, ejabberd_cowboy:start_cowboy(a_ref, Opts, 2, 10)), Result = ejabberd_cowboy:start_cowboy(a_ref_2, Opts, 2, 10), - {error, eaddrinuse} = Result. + ?assertEqual({error, eaddrinuse}, Result). %%-------------------------------------------------------------------- %% Helpers @@ -391,4 +392,3 @@ ws_websocket_terminate(_Reason, _Req, no_ws_state) -> assert_cowboy_handler_calls(M, F, Num) -> Fun = fun() -> meck:num_calls(M, F, '_') end, async_helper:wait_until(Fun, Num). - diff --git a/test/ejabberd_c2s_SUITE.erl b/test/ejabberd_c2s_SUITE.erl deleted file mode 100644 index f5b01b5a338..00000000000 --- a/test/ejabberd_c2s_SUITE.erl +++ /dev/null @@ -1,266 +0,0 @@ --module(ejabberd_c2s_SUITE). --include_lib("eunit/include/eunit.hrl"). --include_lib("exml/include/exml_stream.hrl"). --compile([export_all, nowarn_export_all]). - --define(eq(E, I), ?assertEqual(E, I)). --define(am(E, I), ?assertMatch(E, I)). - --import(config_parser_helper, [config/2]). - -all() -> [ - c2s_start_stop_test, - stream_error_when_invalid_domain, - session_established, - send_error_when_waiting, - c2s_is_killed_when_too_many_messages_in_the_queue - ]. - -init_per_suite(C) -> - {ok, _} = application:ensure_all_started(jid), - application:start(x), - C. - -end_per_suite(C) -> - C. - -init_per_testcase(_TC, C) -> - ejabberd_c2s_SUITE_mocks:setup(), - C. - -end_per_testcase(_TC, C) -> - ejabberd_c2s_SUITE_mocks:teardown(), - C. - -c2s_start_stop_test(_) -> - {ok, C2SPid} = given_c2s_started(), - - when_c2s_is_stopped(C2SPid), - - %% then - ?eq(false, erlang:is_process_alive(C2SPid)). - - -stream_error_when_invalid_domain(_) -> - ok = meck:new(mongoose_hooks, [passthrough]), - {ok, C2SPid} = given_c2s_started(), - - C2Sactions = when_stream_is_opened(C2SPid, stream_header(<<"badhost">>)), - [StreamStart, StreamError, StreamEnd, CloseSocket] = C2Sactions, - History = meck:history(mongoose_hooks), - HookRes = [ok || {_, {mongoose_hooks, xmpp_send_element, [undefined, #{host_type := undefined} | _]}, _} <- History], - ?am([], HookRes), - ?am({send, [_P, - <<"", - "">> - ]}, - StreamStart), - ?am({send, [_P, - <<"", - "", - "">>]}, StreamError), - ?am({send, [_P, <<"">>]}, StreamEnd), - ?am({close, [_P]}, CloseSocket), - ok. - -session_established(_) -> - {ok, C2SPid} = given_c2s_started(), - change_state_to(session_established, C2SPid), - ?eq(session_established, getstate(C2SPid)), - Last = last_stanza(), - ?eq(final_iq_response(), Last). - -send_error_when_waiting(_) -> - % this is a regression test for #1252 - when c2s is in state - % wait_for_session_or_sm and it fails to send a message - % it should be handled properly - {ok, C2SPid} = given_c2s_started(), - change_state_to(wait_for_session_or_sm, C2SPid), - % here we break things to check how c2s will handle error while sending - % message in this state - meck:expect(ejabberd_socket, send, fun(_, _El) -> error_error_error end), - sync_c2s(C2SPid), - p1_fsm:send_event(C2SPid, {xmlstreamelement, setsession_stanza()}), - sync_c2s(C2SPid), - [Close, StreamEnd, StreamError | _] = lists:reverse(received_stanzas()), - ?eq(stream_error_response(), - StreamError), - ?eq(<<"">>, StreamEnd), - ?eq(close, Close), - ok. - -c2s_is_killed_when_too_many_messages_in_the_queue(_) -> - meck:new(ejabberd_c2s, [passthrough]), - MaxQueueSize = 50, - Self = self(), - %% We simulate a long running event during which - %% there will be many messages put into C2S process message queue - meck:expect(ejabberd_c2s, handle_event, - fun(go_to_sleep, StateName, ProcState) -> - Self ! c2s_going_to_sleep, - ct:pal("going to sleep"), - receive continue -> ok end, - {next_state, StateName, ProcState}; - (Event, StateName, ProcState) -> - meck:passthrough([Event, StateName, ProcState]) - end), - {ok, C2SPid} = given_c2s_started(#{max_fsm_queue => MaxQueueSize}), - - %% We want to monitor the c2s process and not being linked to it - Ref = erlang:monitor(process, C2SPid), - erlang:unlink(C2SPid), - - p1_fsm_old:send_all_state_event(C2SPid, go_to_sleep), - - receive c2s_going_to_sleep -> ok end, - - meck:unload(ejabberd_c2s), - - %% We put MaxQueueSize + 1 messages to C2S Process message queue - %% while it is asleep - %% The process will be killed when it wakes up and tries to process - %% next message - - [p1_fsm_old:send_all_state_event(C2SPid, {event, I}) || - I <- lists:seq(1, MaxQueueSize + 1)], - C2SPid ! continue, - - receive - {'DOWN', Ref, process, C2SPid, {process_limit,{max_queue, AllMessages}}} -> - ct:pal("C2S dead due to message_queue_length=~p, max allowed was ~p", - [AllMessages, MaxQueueSize]); - Msg -> - ct:fail("Other msg: ~p", [Msg]) - after timer:seconds(5) -> - {message_queue_len, N} = process_info(C2SPid, message_queue_len), - ct:fail("timeout waiting c2s exit, with message_queue_len = ~p", [N]) - end, - ok. - -last_stanza() -> - [H|_] = lists:reverse(received_stanzas()), - H. - -received_stanzas() -> - Calls = lists:filtermap(filter_calls(ejabberd_socket, [send, close]), - meck:history(ejabberd_socket)), -%% ct:pal("Calls: ~p", [Calls]), - lists:map(fun extract_stanza/1, Calls). - -extract_stanza({_, [_, S]}) -> S; -extract_stanza({close, _}) -> close. - -change_state_to(Target, C2SPid) -> - Curr = getstate(C2SPid), - change_state_to(Curr, Target, C2SPid). - -change_state_to(T, T, _) -> - ok; -change_state_to(wait_for_stream, T, C2SPid) -> - p1_fsm:send_event(C2SPid, stream_header(<<"localhost">>)), - change_state_to(getstate(C2SPid), T, C2SPid); -change_state_to(wait_for_feature_before_auth, T, C2SPid) -> - p1_fsm:send_event(C2SPid, {xmlstreamelement, auth_stanza()}), - change_state_to(getstate(C2SPid), T, C2SPid); -change_state_to(wait_for_feature_after_auth, T, C2SPid) -> - p1_fsm:send_event(C2SPid, {xmlstreamelement, bind_stanza()}), - change_state_to(getstate(C2SPid), T, C2SPid); -change_state_to(wait_for_session_or_sm, T, C2SPid) -> - p1_fsm:send_event(C2SPid, {xmlstreamelement, setsession_stanza()}), - change_state_to(getstate(C2SPid), T, C2SPid); -change_state_to(_, _, _) -> - error. - -getstate(C2SPid) -> - State = sync_c2s(C2SPid), - [_, StateName | _] = State, - StateName. - -when_stream_is_opened(C2SPid, Stanza) -> - p1_fsm:send_event(C2SPid, Stanza), - sync_c2s(C2SPid), - lists:filtermap(filter_calls(ejabberd_socket, [send, close]), - meck:history(ejabberd_socket)). - -filter_calls(_ExpectedMod, Funs) -> - fun({_Pid, MFA, _Return}) -> - maybe_extract_function_with_args(MFA, Funs) - end. - -maybe_extract_function_with_args({_Mod, Fun, Args}, List) -> - case lists:member(Fun, List) of - true -> {true, {Fun, Args}}; - _ -> false - end. - -sync_c2s(C2SPid) -> catch sys:get_state(C2SPid). - -stream_valid_header_response() -> - R = "" - "", - list_to_binary(R). - -stream_header(Domain) -> - #xmlstreamstart{name = <<"stream:stream">>, - attrs = [{<<"to">>, Domain}, - {<<"xml:lang">>, <<"en">>}, - {<<"version">>, <<"1.0">>}, - {<<"xmlns">>, <<"jabber:client">>}, - {<<"xmlns:stream">>, - <<"http://etherx.jabber.org/streams">>}]}. - -auth_stanza() -> - {xmlel, <<"auth">>, - [{<<"xmlns">>, <<"urn:ietf:params:xml:ns:xmpp-sasl">>}, - {<<"mechanism">>, <<"PLAIN">>}], - [{xmlcdata, <<"AGFsaWNFOTkuODk3NzMzAG1hdHlncnlzYQ==">>}]}. - -bind_stanza() -> - {xmlel, <<"iq">>, - [{<<"type">>, <<"set">>}, {<<"id">>, <<"4436">>}], - [{xmlel, <<"bind">>, - [{<<"xmlns">>, <<"urn:ietf:params:xml:ns:xmpp-bind">>}], - [{xmlel, <<"resource">>, [], [{xmlcdata, <<"res1">>}]}]}]}. - -setsession_stanza() -> - {xmlel, <<"iq">>, - [{<<"type">>, <<"set">>}, {<<"id">>, <<"4436">>}], - [{xmlel, <<"session">>, [{<<"xmlns">>, - <<"urn:ietf:params:xml:ns:xmpp-session">>}], - []}]}. - -given_c2s_started() -> - given_c2s_started(#{}). - -given_c2s_started(ExtraOpts) -> - ejabberd_c2s:start_link({ejabberd_socket, self()}, c2s_opts(ExtraOpts)). - -when_c2s_is_stopped(Pid) -> - stop_c2s(Pid), - sync_c2s(Pid). - -c2s_opts(ExtraOpts) -> - config([listen, c2s], ExtraOpts#{access => c2s, - shaper => c2s_shaper, - max_stanza_size => 65536}). - -stop_c2s(C2SPid) when is_pid(C2SPid) -> - _R = ejabberd_c2s:stop(C2SPid). - -jid(Str) -> - jid:from_binary(Str). - -final_iq_response() -> - <<"" - "" - "">>. - -stream_error_response() -> - <<"", - "", - "">>. - diff --git a/test/ejabberd_sm_SUITE.erl b/test/ejabberd_sm_SUITE.erl index aa85adcfa87..4001529c178 100644 --- a/test/ejabberd_sm_SUITE.erl +++ b/test/ejabberd_sm_SUITE.erl @@ -253,16 +253,21 @@ store_info_sends_message_to_the_session_owner(C) -> JID = jid:make_noprep(U, S, R), spawn_link(fun() -> ejabberd_sm:store_info(JID, cc, undefined) end), %% The original process receives a message - receive {store_session_info, - #jid{luser = User, lserver = Server, lresource = Resource}, - K, V, _FromPid} -> - ?eq(U, User), - ?eq(S, Server), - ?eq(R, Resource), - ?eq({cc, undefined}, {K, V}), - ok - after 5000 -> - ct:fail("store_info_sends_message_to_the_session_owner=timeout") + receive + {'$gen_cast', {async, + _Fun, + [#jid{luser = User, lserver = Server, lresource = Resource}, + K, + V]}} -> + ?eq(U, User), + ?eq(S, Server), + ?eq(R, Resource), + ?eq({cc, undefined}, {K, V}), + ok; + Message -> + ct:fail("unexpected message: ~p", [Message]) + after 5000 -> + ct:fail("store_info_sends_message_to_the_session_owner=timeout") end. remove_info_sends_message_to_the_session_owner(C) -> @@ -277,16 +282,20 @@ remove_info_sends_message_to_the_session_owner(C) -> JID = jid:make_noprep(U, S, R), spawn_link(fun() -> ejabberd_sm:remove_info(JID, cc) end), %% The original process receives a message - receive {remove_session_info, - #jid{luser = User, lserver = Server, lresource = Resource}, - Key, _FromPid} -> - ?eq(U, User), - ?eq(S, Server), - ?eq(R, Resource), - ?eq(cc, Key), - ok - after 5000 -> - ct:fail("remove_info_sends_message_to_the_session_owner=timeout") + receive + {'$gen_cast', {async, + _Fun, + [#jid{luser = User, lserver = Server, lresource = Resource}, + Key]}} -> + ?eq(U, User), + ?eq(S, Server), + ?eq(R, Resource), + ?eq(cc, Key), + ok; + Message -> + ct:fail("unexpected message: ~p", [Message]) + after 5000 -> + ct:fail("remove_info_sends_message_to_the_session_owner=timeout") end. delete_session(C) -> @@ -331,8 +340,10 @@ too_much_sessions(_C) -> given_session_opened(AddSid, AddUSR), receive - replaced -> - ok + {'$gen_cast', {exit, <<"Replaced by new connection">>}} -> + ok; + Message -> + ct:fail("Unexpected message: ~p", [Message]) after 10 -> ct:fail("replaced message not sent") end.