./ct_report/coverage/mod_register.COVER.html

1 %%%----------------------------------------------------------------------
2 %%% File : mod_register.erl
3 %%% Author : Alexey Shchepin <alexey@process-one.net>
4 %%% Purpose : Inband registration support
5 %%% Created : 8 Dec 2002 by Alexey Shchepin <alexey@process-one.net>
6 %%%
7 %%%
8 %%% ejabberd, Copyright (C) 2002-2011 ProcessOne
9 %%%
10 %%% This program is free software; you can redistribute it and/or
11 %%% modify it under the terms of the GNU General Public License as
12 %%% published by the Free Software Foundation; either version 2 of the
13 %%% License, or (at your option) any later version.
14 %%%
15 %%% This program is distributed in the hope that it will be useful,
16 %%% but WITHOUT ANY WARRANTY; without even the implied warranty of
17 %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
18 %%% General Public License for more details.
19 %%%
20 %%% You should have received a copy of the GNU General Public License
21 %%% along with this program; if not, write to the Free Software
22 %%% Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
23 %%%
24 %%%----------------------------------------------------------------------
25
26 -module(mod_register).
27 -author('alexey@process-one.net').
28 -xep([{xep, 77}, {version, "2.4"}]).
29 -behaviour(gen_mod).
30 -behaviour(mongoose_module_metrics).
31
32 %% Gen_mod callbacks
33 -export([start/2,
34 stop/1,
35 config_spec/0,
36 supported_features/0]).
37
38 %% IQ and hook handlers
39 -export([c2s_stream_features/3,
40 unauthenticated_iq_register/5,
41 process_iq/5]).
42
43 %% API
44 -export([try_register/6,
45 process_ip_access/1,
46 process_welcome_message/1]).
47
48 -ignore_xref([c2s_stream_features/3, process_iq/5, try_register/6, unauthenticated_iq_register/5]).
49
50 -include("mongoose.hrl").
51 -include("jlib.hrl").
52 -include("mongoose_config_spec.hrl").
53
54 -spec start(mongooseim:host_type(), list()) -> ok.
55 start(HostType, Opts) ->
56 321 IQDisc = gen_mod:get_opt(iqdisc, Opts, one_queue),
57
58 321 [gen_iq_handler:add_iq_handler_for_domain(HostType, ?NS_REGISTER, Component, Fn, #{}, IQDisc) ||
59 321 {Component, Fn} <- iq_handlers()],
60 321 ejabberd_hooks:add(hooks(HostType)),
61
62 321 mnesia:create_table(mod_register_ip,
63 [{ram_copies, [node()]},
64 {local_content, true},
65 {attributes, [key, value]}]),
66 321 mnesia:add_table_copy(mod_register_ip, node(), ram_copies),
67 321 ok.
68
69 -spec stop(mongooseim:host_type()) -> ok.
70 stop(HostType) ->
71 321 ejabberd_hooks:delete(hooks(HostType)),
72 321 [gen_iq_handler:remove_iq_handler_for_domain(HostType, ?NS_REGISTER, Component) ||
73 321 {Component, _Fn} <- iq_handlers()],
74 321 ok.
75
76 iq_handlers() ->
77 642 [{ejabberd_local, fun ?MODULE:process_iq/5}, {ejabberd_sm, fun ?MODULE:process_iq/5}].
78
79 hooks(HostType) ->
80 642 [{c2s_stream_features, HostType, ?MODULE, c2s_stream_features, 50},
81 {c2s_unauthenticated_iq, HostType, ?MODULE, unauthenticated_iq_register, 50}].
82
83 %%%
84 %%% config_spec
85 %%%
86
87 -spec config_spec() -> mongoose_config_spec:config_section().
88 config_spec() ->
89 164 #section{
90 items = #{<<"iqdisc">> => mongoose_config_spec:iqdisc(),
91 <<"access">> => #option{type = atom,
92 validate = access_rule},
93 <<"welcome_message">> => welcome_message_spec(),
94 <<"registration_watchers">> => #list{items = #option{type = binary,
95 validate = jid}},
96 <<"password_strength">> => #option{type = integer,
97 validate = non_negative},
98 <<"ip_access">> => #list{items = ip_access_spec()}
99 }
100 }.
101
102 welcome_message_spec() ->
103 164 #section{
104 items = #{<<"body">> => #option{type = string},
105 <<"subject">> => #option{type = string}},
106 process = fun ?MODULE:process_welcome_message/1
107 }.
108
109 ip_access_spec() ->
110 164 #section{
111 items = #{<<"address">> => #option{type = string,
112 validate = ip_mask},
113 <<"policy">> => #option{type = atom,
114 validate = {enum, [allow, deny]}}
115 },
116 required = all,
117 process = fun ?MODULE:process_ip_access/1
118 }.
119
120 146 supported_features() -> [dynamic_domains].
121
122 process_ip_access(KVs) ->
123 164 {[[{address, Address}], [{policy, Policy}]], []} = proplists:split(KVs, [address, policy]),
124 164 {Policy, Address}.
125
126 process_welcome_message(KVs) ->
127 82 Body = proplists:get_value(body, KVs, ""),
128 82 Subject = proplists:get_value(subject, KVs, ""),
129 82 {Subject, Body}.
130
131 %%%
132 %%% Hooks and IQ handlers
133 %%%
134
135 -spec c2s_stream_features([exml:element()], mongooseim:host_type(), jid:lserver()) ->
136 [exml:element()].
137 c2s_stream_features(Acc, _HostType, _LServer) ->
138 6515 [#xmlel{name = <<"register">>,
139 attrs = [{<<"xmlns">>, ?NS_FEATURE_IQREGISTER}]} | Acc].
140
141 -spec unauthenticated_iq_register(exml:element() | empty, mongooseim:host_type(),
142 jid:server(), jlib:iq(),
143 {inet:ip_address(), inet:port_number()} | undefined) ->
144 exml:element() | empty.
145 unauthenticated_iq_register(_Acc, HostType, Server, #iq{xmlns = ?NS_REGISTER} = IQ, IP) ->
146 380 Address = case IP of
147 380 {A, _Port} -> A;
148
:-(
_ -> undefined
149 end,
150 380 ResIQ = process_unauthenticated_iq(HostType,
151 no_JID,
152 %% For the above: the client is
153 %% not registered (no JID), at
154 %% least not yet, so they can
155 %% not be authenticated either.
156 make_host_only_jid(Server),
157 IQ,
158 Address),
159 380 set_sender(jlib:iq_to_xml(ResIQ), make_host_only_jid(Server));
160 unauthenticated_iq_register(Acc, _HostType, _Server, _IQ, _IP) ->
161
:-(
Acc.
162
163 %% Clients must register before being able to authenticate.
164 process_unauthenticated_iq(HostType, From, To, #iq{type = set} = IQ, IPAddr) ->
165 121 process_iq_set(HostType, From, To, IQ, IPAddr);
166 process_unauthenticated_iq(HostType, From, To, #iq{type = get} = IQ, IPAddr) ->
167 259 process_iq_get(HostType, From, To, IQ, IPAddr).
168
169 -spec process_iq(mongoose_acc:t(), jid:jid(), jid:jid(), jlib:iq(), map())
170 -> {mongoose_acc:t(), jlib:iq()}.
171 process_iq(Acc, From, To, #iq{type = set} = IQ, _Extra) ->
172 110 HostType = mongoose_acc:host_type(Acc),
173 110 Res = process_iq_set(HostType, From, To, IQ, jid:to_lower(From)),
174 110 {Acc, Res};
175 process_iq(Acc, From, To, #iq{type = get} = IQ, _Extra) ->
176 1 HostType = mongoose_acc:host_type(Acc),
177 1 Res = process_iq_get(HostType, From, To, IQ, jid:to_lower(From)),
178 1 {Acc, Res}.
179
180 process_iq_set(HostType, From, To, #iq{sub_el = Child} = IQ, Source) ->
181 231 true = is_query_element(Child),
182 231 handle_set(HostType, IQ, From, To, Source).
183
184 handle_set(HostType, IQ, ClientJID, ServerJID, Source) ->
185 231 #iq{sub_el = Query} = IQ,
186 231 case which_child_elements(Query) of
187 bad_request ->
188 1 error_response(IQ, mongoose_xmpp_errors:bad_request());
189 only_remove_child ->
190 105 attempt_cancelation(HostType, ClientJID, ServerJID, IQ);
191 various_elements_present ->
192 125 case has_username_and_password_children(Query) of
193 true ->
194 125 Credentials = get_username_and_password_values(Query),
195 125 register_or_change_password(HostType, Credentials, ClientJID, ServerJID, IQ, Source);
196 false ->
197
:-(
error_response(IQ, mongoose_xmpp_errors:bad_request())
198 end
199 end.
200
201 which_child_elements(#xmlel{children = C} = Q) when length(C) =:= 1 ->
202 105 case Q#xmlel.children of
203 [#xmlel{name = <<"remove">>}] ->
204 105 only_remove_child;
205 [_] ->
206
:-(
bad_request
207 end;
208 which_child_elements(#xmlel{children = C} = Q) when length(C) > 1 ->
209 126 case exml_query:subelement(Q, <<"remove">>) of
210 #xmlel{name = <<"remove">>} ->
211 1 bad_request;
212 undefined ->
213 125 various_elements_present
214 end;
215 which_child_elements(#xmlel{children = []}) ->
216
:-(
bad_request.
217
218 has_username_and_password_children(Q) ->
219 (undefined =/= exml_query:path(Q, [{element, <<"username">>}]))
220 125 and
221 (undefined =/= exml_query:path(Q, [{element, <<"password">>}])).
222
223 get_username_and_password_values(Q) ->
224 125 {exml_query:path(Q, [{element, <<"username">>}, cdata]),
225 exml_query:path(Q, [{element, <<"password">>}, cdata])}.
226
227 register_or_change_password(HostType, Credentials, ClientJID, #jid{lserver = ServerDomain}, IQ, IPAddr) ->
228 125 {Username, Password} = Credentials,
229 125 case inband_registration_and_cancelation_allowed(HostType, ServerDomain, ClientJID) of
230 true ->
231 125 #iq{sub_el = Children, lang = Lang} = IQ,
232 125 try_register_or_set_password(HostType, Username, ServerDomain, Password,
233 ClientJID, IQ, Children, IPAddr, Lang);
234 false ->
235 %% This is not described in XEP 0077.
236
:-(
error_response(IQ, mongoose_xmpp_errors:forbidden())
237 end.
238
239 attempt_cancelation(HostType, #jid{} = ClientJID, #jid{lserver = ServerDomain}, #iq{} = IQ) ->
240 105 case inband_registration_and_cancelation_allowed(HostType, ServerDomain, ClientJID) of
241 true ->
242 %% The response must be sent *before* the
243 %% XML stream is closed (the call to
244 %% `ejabberd_auth:remove_user/1' does
245 %% this): as it is, when canceling a
246 %% registration, there is no way to deal
247 %% with failure.
248 104 ResIQ = IQ#iq{type = result, sub_el = []},
249 104 ejabberd_router:route(
250 jid:make_noprep(<<>>, <<>>, <<>>),
251 ClientJID,
252 jlib:iq_to_xml(ResIQ)),
253 104 ejabberd_auth:remove_user(ClientJID),
254 104 ignore;
255 false ->
256 1 error_response(IQ, mongoose_xmpp_errors:not_allowed())
257 end.
258
259 inband_registration_and_cancelation_allowed(_HostType, _ServerDomain, no_JID) ->
260 121 true;
261 inband_registration_and_cancelation_allowed(HostType, ServerDomain, JID) ->
262 109 Rule = gen_mod:get_module_opt(HostType, ?MODULE, access, none),
263 109 allow =:= acl:match_rule(HostType, ServerDomain, Rule, JID).
264
265 process_iq_get(_HostType, From, _To, #iq{lang = Lang, sub_el = Child} = IQ, _Source) ->
266 260 true = is_query_element(Child),
267 260 {_IsRegistered, UsernameSubels, QuerySubels} =
268 case From of
269 JID = #jid{user = User} ->
270 1 case ejabberd_auth:does_user_exist(JID) of
271 true ->
272 1 {true, [#xmlcdata{content = User}],
273 [#xmlel{name = <<"registered">>}]};
274 false ->
275
:-(
{false, [#xmlcdata{content = User}], []}
276 end;
277 _ ->
278 259 {false, [], []}
279 end,
280 260 TranslatedMsg = translate:translate(
281 Lang, <<"Choose a username and password to register with this server">>),
282 260 IQ#iq{type = result,
283 sub_el = [#xmlel{name = <<"query">>,
284 attrs = [{<<"xmlns">>, <<"jabber:iq:register">>}],
285 children = [#xmlel{name = <<"instructions">>,
286 children = [#xmlcdata{content = TranslatedMsg}]},
287 #xmlel{name = <<"username">>,
288 children = UsernameSubels},
289 #xmlel{name = <<"password">>}
290 | QuerySubels]}]}.
291
292 try_register_or_set_password(HostType, User, Server, Password, #jid{user = User, lserver = Server} = UserJID,
293 IQ, SubEl, _Source, Lang) ->
294 4 try_set_password(HostType, UserJID, Password, IQ, SubEl, Lang);
295 try_register_or_set_password(HostType, User, Server, Password, _From, IQ, SubEl, Source, Lang) ->
296 121 case check_timeout(Source) of
297 true ->
298 112 case try_register(HostType, User, Server, Password, Source, Lang) of
299 ok ->
300 103 IQ#iq{type = result, sub_el = [SubEl]};
301 {error, Error} ->
302 9 error_response(IQ, [SubEl, Error])
303 end;
304 false ->
305 9 ErrText = <<"Users are not allowed to register accounts so quickly">>,
306 9 error_response(IQ, mongoose_xmpp_errors:resource_constraint(Lang, ErrText))
307 end.
308
309 %% @doc Try to change password and return IQ response
310 try_set_password(HostType, #jid{} = UserJID, Password, IQ, SubEl, Lang) ->
311 4 case is_strong_password(HostType, Password) of
312 true ->
313 4 case ejabberd_auth:set_password(UserJID, Password) of
314 ok ->
315 2 IQ#iq{type = result, sub_el = [SubEl]};
316 {error, empty_password} ->
317 2 error_response(IQ, [SubEl, mongoose_xmpp_errors:bad_request()]);
318 {error, not_allowed} ->
319
:-(
error_response(IQ, [SubEl, mongoose_xmpp_errors:not_allowed()]);
320 {error, invalid_jid} ->
321
:-(
error_response(IQ, [SubEl, mongoose_xmpp_errors:item_not_found()])
322 end;
323 false ->
324
:-(
ErrText = <<"The password is too weak">>,
325
:-(
error_response(IQ, [SubEl, mongoose_xmpp_errors:not_acceptable(Lang, ErrText)])
326 end.
327
328 try_register(HostType, User, Server, Password, SourceRaw, Lang) ->
329 112 case jid:is_nodename(User) of
330 false ->
331
:-(
{error, mongoose_xmpp_errors:bad_request()};
332 _ ->
333 112 JID = jid:make(User, Server, <<>>),
334 112 Access = gen_mod:get_module_opt(HostType, ?MODULE, access, all),
335 112 IPAccess = get_ip_access(HostType),
336 112 case {acl:match_rule(HostType, Server, Access, JID),
337 check_ip_access(SourceRaw, IPAccess)} of
338 {deny, _} ->
339 1 {error, mongoose_xmpp_errors:forbidden()};
340 {_, deny} ->
341
:-(
{error, mongoose_xmpp_errors:forbidden()};
342 {allow, allow} ->
343 111 verify_password_and_register(HostType, JID, Password, SourceRaw, Lang)
344 end
345 end.
346
347 verify_password_and_register(HostType, #jid{} = JID, Password, SourceRaw, Lang) ->
348 111 case is_strong_password(HostType, Password) of
349 true ->
350 111 case ejabberd_auth:try_register(JID, Password) of
351 {error, exists} ->
352 7 {error, mongoose_xmpp_errors:conflict()};
353 {error, invalid_jid} ->
354
:-(
{error, mongoose_xmpp_errors:jid_malformed()};
355 {error, not_allowed} ->
356
:-(
{error, mongoose_xmpp_errors:not_allowed()};
357 {error, null_password} ->
358 1 {error, mongoose_xmpp_errors:not_acceptable()};
359 _ ->
360 103 send_welcome_message(HostType, JID),
361 103 send_registration_notifications(HostType, JID, SourceRaw),
362 103 ok
363 end;
364 false ->
365
:-(
ErrText = <<"The password is too weak">>,
366
:-(
{error, mongoose_xmpp_errors:not_acceptable(Lang, ErrText)}
367 end.
368
369 send_welcome_message(HostType, #jid{lserver = Server} = JID) ->
370 103 case gen_mod:get_module_opt(HostType, ?MODULE, welcome_message, {"", ""}) of
371 {"", ""} ->
372 103 ok;
373 {Subj, Body} ->
374
:-(
ejabberd_router:route(
375 jid:make_noprep(<<>>, Server, <<>>),
376 JID,
377 #xmlel{name = <<"message">>, attrs = [{<<"type">>, <<"normal">>}],
378 children = [#xmlel{name = <<"subject">>,
379 children = [#xmlcdata{content = Subj}]},
380 #xmlel{name = <<"body">>,
381 children = [#xmlcdata{content = Body}]}]});
382 _ ->
383
:-(
ok
384 end.
385
386 send_registration_notifications(HostType, #jid{lserver = Domain} = UJID, Source) ->
387 103 case gen_mod:get_module_opt(HostType, ?MODULE, registration_watchers, []) of
388 101 [] -> ok;
389 JIDs when is_list(JIDs) ->
390 2 Body = lists:flatten(
391 io_lib:format(
392 "[~s] The account ~s was registered from IP address ~s "
393 "on node ~w using ~p.",
394 [get_time_string(), jid:to_binary(UJID),
395 ip_to_string(Source), node(), ?MODULE])),
396 2 lists:foreach(fun(S) -> send_registration_notification(S, Domain, Body) end, JIDs);
397 _ ->
398
:-(
ok
399 end.
400
401 send_registration_notification(JIDBin, Domain, Body) ->
402 2 case jid:from_binary(JIDBin) of
403
:-(
error -> ok;
404 JID ->
405 2 Message = #xmlel{name = <<"message">>,
406 attrs = [{<<"type">>, <<"chat">>}],
407 children = [#xmlel{name = <<"body">>,
408 children = [#xmlcdata{content = Body}]}]},
409 2 ejabberd_router:route(jid:make_noprep(<<>>, Domain, <<>>), JID, Message)
410 end.
411
412 check_timeout(undefined) ->
413
:-(
true;
414 check_timeout(Source) ->
415 121 Timeout = mongoose_config:get_opt(registration_timeout),
416 121 case is_integer(Timeout) of
417 true ->
418 12 Priority = -(erlang:system_time(second)),
419 12 CleanPriority = Priority + Timeout,
420 12 F = fun() -> check_and_store_ip_entry(Source, Priority, CleanPriority) end,
421
422 12 case mnesia:transaction(F) of
423 {atomic, Res} ->
424 12 Res;
425 {aborted, Reason} ->
426
:-(
?LOG_ERROR(#{what => reg_check_timeout_failed,
427
:-(
reg_source => Source, reason => Reason}),
428
:-(
true
429 end;
430 false ->
431 109 true
432 end.
433
434 check_and_store_ip_entry(Source, Priority, CleanPriority) ->
435 12 Treap = case mnesia:read(mod_register_ip, treap, write) of
436 [] ->
437 1 treap:empty();
438 11 [{mod_register_ip, treap, T}] -> T
439 end,
440 12 Treap1 = clean_treap(Treap, CleanPriority),
441 12 case treap:lookup(Source, Treap1) of
442 error ->
443 3 Treap2 = treap:insert(Source, Priority, [],
444 Treap1),
445 3 mnesia:write({mod_register_ip, treap, Treap2}),
446 3 true;
447 {ok, _, _} ->
448 9 mnesia:write({mod_register_ip, treap, Treap1}),
449 9 false
450 end.
451
452 clean_treap(Treap, CleanPriority) ->
453 14 case treap:is_empty(Treap) of
454 true ->
455 3 Treap;
456 false ->
457 11 {_Key, Priority, _Value} = treap:get_root(Treap),
458 11 case Priority > CleanPriority of
459 2 true -> clean_treap(treap:delete_root(Treap), CleanPriority);
460 9 false -> Treap
461 end
462 end.
463
464 2 ip_to_string(Source) when is_tuple(Source) -> inet_parse:ntoa(Source);
465
:-(
ip_to_string(undefined) -> "undefined";
466
:-(
ip_to_string(_) -> "unknown".
467
468 2 get_time_string() -> write_time(erlang:localtime()).
469 %% Function copied from ejabberd_logger_h.erl and customized
470 write_time({{Y, Mo, D}, {H, Mi, S}}) ->
471 2 io_lib:format("~w-~.2.0w-~.2.0w ~.2.0w:~.2.0w:~.2.0w",
472 [Y, Mo, D, H, Mi, S]).
473
474 is_strong_password(HostType, Password) ->
475 115 case gen_mod:get_module_opt(HostType, ?MODULE, password_strength, 0) of
476 Entropy when is_number(Entropy), Entropy == 0 ->
477 115 true;
478 Entropy when is_number(Entropy), Entropy > 0 ->
479
:-(
ejabberd_auth:entropy(Password) >= Entropy;
480 Wrong ->
481
:-(
?LOG_WARNING(#{what => reg_wrong_password_strength,
482
:-(
host => HostType, value => Wrong}),
483
:-(
true
484 end.
485
486 %%%
487 %%% ip_access management
488 %%%
489
490 get_ip_access(HostType) ->
491 112 IPAccess = gen_mod:get_module_opt(HostType, ?MODULE, ip_access, []),
492 112 lists:flatmap(
493 fun({Access, {IP, Mask}}) ->
494
:-(
[{Access, IP, Mask}];
495 ({Access, S}) ->
496 224 case mongoose_lib:parse_ip_netmask(S) of
497 {ok, {IP, Mask}} ->
498 224 [{Access, IP, Mask}];
499 error ->
500
:-(
?LOG_ERROR(#{what => reg_invalid_network_specification,
501
:-(
specification => S}),
502
:-(
[]
503 end
504 end, IPAccess).
505
506 check_ip_access(_Source, []) ->
507
:-(
allow;
508 check_ip_access({User, Server, Resource}, IPAccess) ->
509
:-(
case ejabberd_sm:get_session_ip(jid:make(User, Server, Resource)) of
510
:-(
{IPAddress, _PortNumber} -> check_ip_access(IPAddress, IPAccess);
511
:-(
_ -> true
512 end;
513 check_ip_access({_, _, _, _} = IP,
514 [{Access, {_, _, _, _} = Net, Mask} | IPAccess]) ->
515 112 IPInt = ip_to_integer(IP),
516 112 NetInt = ip_to_integer(Net),
517 112 M = bnot ((1 bsl (32 - Mask)) - 1),
518 112 case IPInt band M =:= NetInt band M of
519 112 true -> Access;
520
:-(
false -> check_ip_access(IP, IPAccess)
521 end;
522 check_ip_access({_, _, _, _, _, _, _, _} = IP,
523 [{Access, {_, _, _, _, _, _, _, _} = Net, Mask} | IPAccess]) ->
524
:-(
IPInt = ip_to_integer(IP),
525
:-(
NetInt = ip_to_integer(Net),
526
:-(
M = bnot ((1 bsl (128 - Mask)) - 1),
527
:-(
case IPInt band M =:= NetInt band M of
528
:-(
true -> Access;
529
:-(
false -> check_ip_access(IP, IPAccess)
530 end;
531 check_ip_access(IP, [_ | IPAccess]) ->
532
:-(
check_ip_access(IP, IPAccess).
533
534 ip_to_integer({IP1, IP2, IP3, IP4}) ->
535 224 <<X:32>> = <<IP1, IP2, IP3, IP4>>,
536 224 X;
537 ip_to_integer({IP1, IP2, IP3, IP4, IP5, IP6, IP7, IP8}) ->
538
:-(
<<X:64>> = <<IP1, IP2, IP3, IP4, IP5, IP6, IP7, IP8>>,
539
:-(
X.
540
541 make_host_only_jid(Name) when is_binary(Name) ->
542 760 jid:make(<<>>, Name, <<>>).
543
544 set_sender(#xmlel{attrs = A} = Stanza, #jid{} = From) ->
545 380 Stanza#xmlel{attrs = [{<<"from">>, jid:to_binary(From)}|A]}.
546
547 is_query_element(#xmlel{name = <<"query">>}) ->
548 491 true;
549 is_query_element(_) ->
550
:-(
false.
551
552 error_response(Request, Reasons) when is_list(Reasons) ->
553 11 Request#iq{type = error, sub_el = Reasons};
554 error_response(Request, Reason) ->
555 11 Request#iq{type = error, sub_el = Reason}.
Line Hits Source