./ct_report/coverage/mod_ping.COVER.html

1 %%%----------------------------------------------------------------------
2 %%% File : mod_ping.erl
3 %%% Author : Piotr Nosek <piotr.nosek@erlang-solutions.com>
4 %%% Purpose : XEP-0199 XMPP Ping implementation
5 %%% Created : 14 Nov 2019 by Piotr Nosek <piotr.nosek@erlang-solutions.com>
6 %%%----------------------------------------------------------------------
7
8 -module(mod_ping).
9 -author('piotr.nosek@erlang-solutions.com').
10
11 -behavior(gen_mod).
12 -xep([{xep, 199}, {version, "2.0.1"}]).
13
14 -include("jlib.hrl").
15 -include("mongoose_logger.hrl").
16 -include("mongoose_config_spec.hrl").
17
18 -define(DEFAULT_SEND_PINGS, false). % bool()
19 -define(DEFAULT_PING_INTERVAL, (60*1000)). % 60 seconds
20 -define(DEFAULT_PING_REQ_TIMEOUT, (32*1000)).% 32 seconds
21
22 %% gen_mod callbacks
23 -export([start/2,
24 stop/1,
25 config_spec/0,
26 supported_features/0]).
27
28 %% Hook callbacks
29 -export([user_send_packet/3,
30 user_send_iq/3,
31 user_ping_response/3,
32 filter_local_packet/3,
33 iq_ping/5]).
34
35 %% Record that will be stored in the c2s state when the server pings the client,
36 %% in order to indentify the possible client's answer.
37 -record(ping_handler, {id :: binary(), time :: integer()}).
38
39 %%====================================================================
40 %% Info Handler
41 %%====================================================================
42
43 hooks(HostType) ->
44 5 [{user_ping_response, HostType, fun ?MODULE:user_ping_response/3, #{}, 100},
45 {filter_local_packet, HostType, fun ?MODULE:filter_local_packet/3, #{}, 100}
46 | c2s_hooks(HostType)].
47
48 -spec c2s_hooks(mongooseim:host_type()) -> gen_hook:hook_list(mongoose_c2s_hooks:fn()).
49 c2s_hooks(HostType) ->
50 5 [
51 {user_send_packet, HostType, fun ?MODULE:user_send_packet/3, #{}, 100},
52 {user_send_iq, HostType, fun ?MODULE:user_send_iq/3, #{}, 100}
53 ].
54
55 ensure_metrics(HostType) ->
56 3 mongoose_metrics:ensure_metric(HostType, [mod_ping, ping_response], spiral),
57 3 mongoose_metrics:ensure_metric(HostType, [mod_ping, ping_response_timeout], spiral),
58 3 mongoose_metrics:ensure_metric(HostType, [mod_ping, ping_response_time], histogram).
59
60 %%====================================================================
61 %% gen_mod callbacks
62 %%====================================================================
63
64 -spec start(mongooseim:host_type(), gen_mod:module_opts()) -> ok.
65 start(HostType, #{send_pings := SendPings, iqdisc := IQDisc}) ->
66 3 ensure_metrics(HostType),
67 3 gen_iq_handler:add_iq_handler_for_domain(
68 HostType, ?NS_PING, ejabberd_sm, fun ?MODULE:iq_ping/5, #{}, IQDisc),
69 3 gen_iq_handler:add_iq_handler_for_domain(
70 HostType, ?NS_PING, ejabberd_local, fun ?MODULE:iq_ping/5, #{}, IQDisc),
71 3 maybe_add_hooks_handlers(HostType, SendPings).
72
73 -spec maybe_add_hooks_handlers(mongooseim:host_type(), boolean()) -> ok.
74 maybe_add_hooks_handlers(Host, true) ->
75 2 gen_hook:add_handlers(hooks(Host));
76 maybe_add_hooks_handlers(_, _) ->
77 1 ok.
78
79 -spec stop(mongooseim:host_type()) -> ok.
80 stop(HostType) ->
81 %% a word of warning: timers are installed in c2s processes, so stopping mod_ping
82 %% won't stop currently running timers. They'll run one more time, and then stop.
83 3 gen_hook:delete_handlers(hooks(HostType)),
84 3 gen_iq_handler:remove_iq_handler_for_domain(HostType, ?NS_PING, ejabberd_local),
85 3 gen_iq_handler:remove_iq_handler_for_domain(HostType, ?NS_PING, ejabberd_sm),
86 3 ok.
87
88 -spec config_spec() -> mongoose_config_spec:config_section().
89 config_spec() ->
90 104 #section{
91 items = #{<<"send_pings">> => #option{type = boolean},
92 <<"ping_interval">> => #option{type = integer,
93 validate = positive,
94 process = fun timer:seconds/1},
95 <<"timeout_action">> => #option{type = atom,
96 validate = {enum, [none, kill]}},
97 <<"ping_req_timeout">> => #option{type = integer,
98 validate = positive,
99 process = fun timer:seconds/1},
100 <<"iqdisc">> => mongoose_config_spec:iqdisc()
101 },
102 defaults = #{<<"send_pings">> => ?DEFAULT_SEND_PINGS,
103 <<"ping_interval">> => ?DEFAULT_PING_INTERVAL,
104 <<"timeout_action">> => none,
105 <<"ping_req_timeout">> => ?DEFAULT_PING_REQ_TIMEOUT,
106 <<"iqdisc">> => no_queue
107 }
108 }.
109
110
:-(
supported_features() -> [dynamic_domains].
111
112 %%====================================================================
113 %% IQ handlers
114 %%====================================================================
115 iq_ping(Acc, _From, _To, #iq{type = get, sub_el = #xmlel{name = <<"ping">>}} = IQ, _) ->
116 5 {Acc, IQ#iq{type = result, sub_el = []}};
117 iq_ping(Acc, _From, _To, #iq{sub_el = SubEl} = IQ, _) ->
118 2 NewSubEl = [SubEl, mongoose_xmpp_errors:feature_not_implemented()],
119 2 {Acc, IQ#iq{type = error, sub_el = NewSubEl}}.
120
121 %%====================================================================
122 %% Hook callbacks
123 %%====================================================================
124
125 -spec filter_local_packet(Acc, Params, Extra) -> {ok, Acc} | {stop, drop} when
126 Acc :: mongoose_hooks:filter_packet_acc(),
127 Params :: map(),
128 Extra :: gen_hook:extra().
129 filter_local_packet({_, _, _, Stanza} = Acc, _Params, _Extra) ->
130 138 case is_ping_error(Stanza) of
131 true ->
132
:-(
?LOG_DEBUG(#{what => ping_error_received, acc => Acc}),
133
:-(
{stop, drop};
134 false ->
135 138 {ok, Acc}
136 end.
137
138 -spec user_send_iq(mongoose_acc:t(), mongoose_c2s_hooks:params(), gen_hook:extra()) ->
139 mongoose_c2s_hooks:result().
140 user_send_iq(Acc, #{c2s_data := StateData}, #{host_type := HostType}) ->
141 44 StanzaType = mongoose_acc:stanza_type(Acc),
142 44 ModState = mongoose_c2s:get_mod_state(StateData, ?MODULE),
143 44 handle_stanza(StanzaType, ModState, Acc, StateData, HostType).
144
145 handle_stanza(Type, {ok, PingHandler}, Acc, StateData, HostType) when Type == <<"result">>;
146 Type == <<"error">> ->
147 12 handle_ping_response(Type, PingHandler, Acc, StateData, HostType);
148 handle_stanza(_, _, Acc, _, _) ->
149 32 {ok, Acc}.
150
151 handle_ping_response(Type, #ping_handler{id = PingId, time = T0}, Acc, StateData, HostType) ->
152 12 IqResponse = mongoose_acc:element(Acc),
153 12 IqId = exml_query:attr(IqResponse, <<"id">>),
154 12 case IqId of
155 PingId ->
156 12 Jid = mongoose_c2s:get_jid(StateData),
157 12 TDelta = erlang:monotonic_time(millisecond) - T0,
158 12 mongoose_hooks:user_ping_response(HostType, #{}, Jid, IqResponse, TDelta),
159 12 Action = determine_action(Type),
160 12 {stop, mongoose_c2s_acc:to_acc(Acc, actions, Action)};
161 _ ->
162
:-(
{ok, Acc}
163 end.
164
165 determine_action(<<"result">>) ->
166 10 {{timeout, ping_timeout}, cancel};
167 determine_action(<<"error">>) ->
168 2 [{{timeout, ping_timeout}, cancel}, {{timeout, ping_error}, 0, fun ping_c2s_handler/2}].
169
170 -spec user_send_packet(mongoose_acc:t(), mongoose_c2s_hooks:params(), gen_hook:extra()) ->
171 mongoose_c2s_hooks:result().
172 user_send_packet(Acc, _Params, #{host_type := HostType}) ->
173 68 Interval = gen_mod:get_module_opt(HostType, ?MODULE, ping_interval),
174 68 Action = {{timeout, ping}, Interval, fun ping_c2s_handler/2},
175 68 {ok, mongoose_c2s_acc:to_acc(Acc, actions, Action)}.
176
177 -spec ping_c2s_handler(atom(), mongoose_c2s:data()) -> mongoose_c2s_acc:t().
178 ping_c2s_handler(ping, StateData) ->
179 16 HostType = mongoose_c2s:get_host_type(StateData),
180 16 Interval = gen_mod:get_module_opt(HostType, ?MODULE, ping_req_timeout),
181 16 Actions = [{{timeout, send_ping}, Interval, fun ping_c2s_handler/2}],
182 16 mongoose_c2s_acc:new(#{actions => Actions});
183 ping_c2s_handler(send_ping, StateData) ->
184 14 PingId = mongoose_bin:gen_from_crypto(),
185 14 IQ = ping_get(PingId),
186 14 HostType = mongoose_c2s:get_host_type(StateData),
187 14 LServer = mongoose_c2s:get_lserver(StateData),
188 14 Jid = mongoose_c2s:get_jid(StateData),
189 14 FromServer = jid:make_noprep(<<>>, LServer, <<>>),
190 14 Interval = gen_mod:get_module_opt(HostType, ?MODULE, ping_req_timeout),
191 14 Actions = [{{timeout, ping_timeout}, Interval, fun ping_c2s_handler/2}],
192 14 T0 = erlang:monotonic_time(millisecond),
193 14 Params = #{host_type => HostType, lserver => LServer, location => ?LOCATION,
194 from_jid => FromServer, to_jid => Jid, element => IQ},
195 14 Acc = mongoose_acc:new(Params),
196 14 mongoose_c2s_acc:new(#{state_mod => #{?MODULE => #ping_handler{id = PingId, time = T0}},
197 actions => Actions, route => [Acc]});
198 ping_c2s_handler(ping_timeout, StateData) ->
199 2 Jid = mongoose_c2s:get_jid(StateData),
200 2 HostType = mongoose_c2s:get_host_type(StateData),
201 2 mongoose_hooks:user_ping_response(HostType, #{}, Jid, timeout, 0),
202 2 handle_ping_action(HostType, ping_timeout);
203 ping_c2s_handler(ping_error, StateData) ->
204 2 HostType = mongoose_c2s:get_host_type(StateData),
205 2 handle_ping_action(HostType, ping_error).
206
207 handle_ping_action(HostType, Reason) ->
208 4 TimeoutAction = gen_mod:get_module_opt(HostType, ?MODULE, timeout_action),
209 4 case TimeoutAction of
210 2 kill -> mongoose_c2s_acc:new(#{stop => {shutdown, Reason}});
211 2 _ -> mongoose_c2s_acc:new()
212 end.
213
214 -spec user_ping_response(Acc, Params, Extra) -> {ok, Acc} when
215 Acc :: mongoose_acc:t(),
216 Params :: #{response := timeout | jlib:iq(), time_delta := non_neg_integer()},
217 Extra :: #{host_type := mongooseim:host_type()}.
218 user_ping_response(Acc, #{response := timeout}, #{host_type := HostType}) ->
219 2 mongoose_metrics:update(HostType, [mod_ping, ping_response_timeout], 1),
220 2 {ok, Acc};
221 user_ping_response(Acc, #{time_delta := TDelta}, #{host_type := HostType}) ->
222 12 mongoose_metrics:update(HostType, [mod_ping, ping_response_time], TDelta),
223 12 mongoose_metrics:update(HostType, [mod_ping, ping_response], 1),
224 12 {ok, Acc}.
225
226 %%====================================================================
227 %% Stanzas
228 %%====================================================================
229
230 -spec ping_get(binary()) -> exml:element().
231 ping_get(Id) ->
232 14 #xmlel{name = <<"iq">>,
233 attrs = [{<<"type">>, <<"get">>}, {<<"id">>, Id}],
234 children = [#xmlel{name = <<"ping">>, attrs = [{<<"xmlns">>, ?NS_PING}]}]}.
235
236 -spec is_ping_error(exml:element()) -> boolean().
237 is_ping_error(Stanza) ->
238 138 case exml_query:attr(Stanza, <<"type">>) of
239 <<"error">> ->
240 2 undefined =/= exml_query:subelement_with_name_and_ns(Stanza, <<"ping">>, ?NS_PING)
241 andalso
242
:-(
undefined =/= exml_query:subelement(Stanza, <<"error">>);
243 _ ->
244 136 false
245 end.
Line Hits Source