./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(), gen_mod:module_opts()) -> ok.
55 start(HostType, #{iqdisc := IQDisc}) ->
56 324 [gen_iq_handler:add_iq_handler_for_domain(HostType, ?NS_REGISTER, Component, Fn, #{}, IQDisc) ||
57 324 {Component, Fn} <- iq_handlers()],
58 324 ejabberd_hooks:add(hooks(HostType)),
59
60 324 mnesia:create_table(mod_register_ip,
61 [{ram_copies, [node()]},
62 {local_content, true},
63 {attributes, [key, value]}]),
64 324 mnesia:add_table_copy(mod_register_ip, node(), ram_copies),
65 324 ok.
66
67 -spec stop(mongooseim:host_type()) -> ok.
68 stop(HostType) ->
69 324 ejabberd_hooks:delete(hooks(HostType)),
70 324 [gen_iq_handler:remove_iq_handler_for_domain(HostType, ?NS_REGISTER, Component) ||
71 324 {Component, _Fn} <- iq_handlers()],
72 324 ok.
73
74 iq_handlers() ->
75 648 [{ejabberd_local, fun ?MODULE:process_iq/5}, {ejabberd_sm, fun ?MODULE:process_iq/5}].
76
77 hooks(HostType) ->
78 648 [{c2s_stream_features, HostType, ?MODULE, c2s_stream_features, 50},
79 {c2s_unauthenticated_iq, HostType, ?MODULE, unauthenticated_iq_register, 50}].
80
81 %%%
82 %%% config_spec
83 %%%
84
85 -spec config_spec() -> mongoose_config_spec:config_section().
86 config_spec() ->
87 166 #section{
88 items = #{<<"iqdisc">> => mongoose_config_spec:iqdisc(),
89 <<"access">> => #option{type = atom,
90 validate = access_rule},
91 <<"welcome_message">> => welcome_message_spec(),
92 <<"registration_watchers">> => #list{items = #option{type = binary,
93 validate = jid}},
94 <<"password_strength">> => #option{type = integer,
95 validate = non_negative},
96 <<"ip_access">> => #list{items = ip_access_spec()}
97 },
98 defaults = #{<<"iqdisc">> => one_queue,
99 <<"access">> => all,
100 <<"registration_watchers">> => [],
101 <<"password_strength">> => 0,
102 <<"ip_access">> => []}
103 }.
104
105 welcome_message_spec() ->
106 166 #section{
107 items = #{<<"body">> => #option{type = string},
108 <<"subject">> => #option{type = string}},
109 defaults = #{<<"body">> => "",
110 <<"subject">> => ""},
111 process = fun ?MODULE:process_welcome_message/1
112 }.
113
114 ip_access_spec() ->
115 166 #section{
116 items = #{<<"address">> => #option{type = string,
117 validate = ip_mask},
118 <<"policy">> => #option{type = atom,
119 validate = {enum, [allow, deny]}}
120 },
121 required = all,
122 process = fun ?MODULE:process_ip_access/1
123 }.
124
125 148 supported_features() -> [dynamic_domains].
126
127 process_ip_access(#{policy := Policy, address := Address}) ->
128 166 {Policy, Address}.
129
130 process_welcome_message(#{subject := Subject, body := Body}) ->
131
:-(
{Subject, Body}.
132
133 %%%
134 %%% Hooks and IQ handlers
135 %%%
136
137 -spec c2s_stream_features([exml:element()], mongooseim:host_type(), jid:lserver()) ->
138 [exml:element()].
139 c2s_stream_features(Acc, _HostType, _LServer) ->
140 7182 [#xmlel{name = <<"register">>,
141 attrs = [{<<"xmlns">>, ?NS_FEATURE_IQREGISTER}]} | Acc].
142
143 -spec unauthenticated_iq_register(exml:element() | empty, mongooseim:host_type(),
144 jid:server(), jlib:iq(),
145 {inet:ip_address(), inet:port_number()} | undefined) ->
146 exml:element() | empty.
147 unauthenticated_iq_register(_Acc, HostType, Server, #iq{xmlns = ?NS_REGISTER} = IQ, IP) ->
148 382 Address = case IP of
149 382 {A, _Port} -> A;
150
:-(
_ -> undefined
151 end,
152 382 ResIQ = process_unauthenticated_iq(HostType,
153 no_JID,
154 %% For the above: the client is
155 %% not registered (no JID), at
156 %% least not yet, so they can
157 %% not be authenticated either.
158 make_host_only_jid(Server),
159 IQ,
160 Address),
161 382 set_sender(jlib:iq_to_xml(ResIQ), make_host_only_jid(Server));
162 unauthenticated_iq_register(Acc, _HostType, _Server, _IQ, _IP) ->
163
:-(
Acc.
164
165 %% Clients must register before being able to authenticate.
166 process_unauthenticated_iq(HostType, From, To, #iq{type = set} = IQ, IPAddr) ->
167 122 process_iq_set(HostType, From, To, IQ, IPAddr);
168 process_unauthenticated_iq(HostType, From, To, #iq{type = get} = IQ, IPAddr) ->
169 260 process_iq_get(HostType, From, To, IQ, IPAddr).
170
171 -spec process_iq(mongoose_acc:t(), jid:jid(), jid:jid(), jlib:iq(), map())
172 -> {mongoose_acc:t(), jlib:iq()}.
173 process_iq(Acc, From, To, #iq{type = set} = IQ, _Extra) ->
174 115 HostType = mongoose_acc:host_type(Acc),
175 115 Res = process_iq_set(HostType, From, To, IQ, jid:to_lower(From)),
176 115 {Acc, Res};
177 process_iq(Acc, From, To, #iq{type = get} = IQ, _Extra) ->
178 1 HostType = mongoose_acc:host_type(Acc),
179 1 Res = process_iq_get(HostType, From, To, IQ, jid:to_lower(From)),
180 1 {Acc, Res}.
181
182 process_iq_set(HostType, From, To, #iq{sub_el = Child} = IQ, Source) ->
183 237 true = is_query_element(Child),
184 237 handle_set(HostType, IQ, From, To, Source).
185
186 handle_set(HostType, IQ, ClientJID, ServerJID, Source) ->
187 237 #iq{sub_el = Query} = IQ,
188 237 case which_child_elements(Query) of
189 bad_request ->
190 1 error_response(IQ, mongoose_xmpp_errors:bad_request());
191 only_remove_child ->
192 110 attempt_cancelation(HostType, ClientJID, ServerJID, IQ);
193 various_elements_present ->
194 126 case has_username_and_password_children(Query) of
195 true ->
196 126 Credentials = get_username_and_password_values(Query),
197 126 register_or_change_password(HostType, Credentials, ClientJID, ServerJID, IQ, Source);
198 false ->
199
:-(
error_response(IQ, mongoose_xmpp_errors:bad_request())
200 end
201 end.
202
203 which_child_elements(#xmlel{children = C} = Q) when length(C) =:= 1 ->
204 110 case Q#xmlel.children of
205 [#xmlel{name = <<"remove">>}] ->
206 110 only_remove_child;
207 [_] ->
208
:-(
bad_request
209 end;
210 which_child_elements(#xmlel{children = C} = Q) when length(C) > 1 ->
211 127 case exml_query:subelement(Q, <<"remove">>) of
212 #xmlel{name = <<"remove">>} ->
213 1 bad_request;
214 undefined ->
215 126 various_elements_present
216 end;
217 which_child_elements(#xmlel{children = []}) ->
218
:-(
bad_request.
219
220 has_username_and_password_children(Q) ->
221 (undefined =/= exml_query:path(Q, [{element, <<"username">>}]))
222 126 and
223 (undefined =/= exml_query:path(Q, [{element, <<"password">>}])).
224
225 get_username_and_password_values(Q) ->
226 126 {exml_query:path(Q, [{element, <<"username">>}, cdata]),
227 exml_query:path(Q, [{element, <<"password">>}, cdata])}.
228
229 register_or_change_password(HostType, Credentials, ClientJID, #jid{lserver = ServerDomain}, IQ, IPAddr) ->
230 126 {Username, Password} = Credentials,
231 126 case inband_registration_and_cancelation_allowed(HostType, ServerDomain, ClientJID) of
232 true ->
233 126 #iq{sub_el = Children, lang = Lang} = IQ,
234 126 try_register_or_set_password(HostType, Username, ServerDomain, Password,
235 ClientJID, IQ, Children, IPAddr, Lang);
236 false ->
237 %% This is not described in XEP 0077.
238
:-(
error_response(IQ, mongoose_xmpp_errors:forbidden())
239 end.
240
241 attempt_cancelation(HostType, #jid{} = ClientJID, #jid{lserver = ServerDomain}, #iq{} = IQ) ->
242 110 case inband_registration_and_cancelation_allowed(HostType, ServerDomain, ClientJID) of
243 true ->
244 %% The response must be sent *before* the
245 %% XML stream is closed (the call to
246 %% `ejabberd_auth:remove_user/1' does
247 %% this): as it is, when canceling a
248 %% registration, there is no way to deal
249 %% with failure.
250 109 ResIQ = IQ#iq{type = result, sub_el = []},
251 109 ejabberd_router:route(
252 jid:make_noprep(<<>>, <<>>, <<>>),
253 ClientJID,
254 jlib:iq_to_xml(ResIQ)),
255 109 ejabberd_auth:remove_user(ClientJID),
256 109 ignore;
257 false ->
258 1 error_response(IQ, mongoose_xmpp_errors:not_allowed())
259 end.
260
261 inband_registration_and_cancelation_allowed(_HostType, _ServerDomain, no_JID) ->
262 122 true;
263 inband_registration_and_cancelation_allowed(HostType, ServerDomain, JID) ->
264 114 Rule = gen_mod:get_module_opt(HostType, ?MODULE, access),
265 114 allow =:= acl:match_rule(HostType, ServerDomain, Rule, JID).
266
267 process_iq_get(_HostType, From, _To, #iq{lang = Lang, sub_el = Child} = IQ, _Source) ->
268 261 true = is_query_element(Child),
269 261 {_IsRegistered, UsernameSubels, QuerySubels} =
270 case From of
271 JID = #jid{luser = LUser} ->
272 1 case ejabberd_auth:does_user_exist(JID) of
273 true ->
274 1 {true, [#xmlcdata{content = LUser}],
275 [#xmlel{name = <<"registered">>}]};
276 false ->
277
:-(
{false, [#xmlcdata{content = LUser}], []}
278 end;
279 _ ->
280 260 {false, [], []}
281 end,
282 261 TranslatedMsg = translate:translate(
283 Lang, <<"Choose a username and password to register with this server">>),
284 261 IQ#iq{type = result,
285 sub_el = [#xmlel{name = <<"query">>,
286 attrs = [{<<"xmlns">>, <<"jabber:iq:register">>}],
287 children = [#xmlel{name = <<"instructions">>,
288 children = [#xmlcdata{content = TranslatedMsg}]},
289 #xmlel{name = <<"username">>,
290 children = UsernameSubels},
291 #xmlel{name = <<"password">>}
292 | QuerySubels]}]}.
293
294 try_register_or_set_password(HostType, LUser, Server, Password, #jid{luser = LUser, lserver = Server} = UserJID,
295 IQ, SubEl, _Source, Lang) ->
296 4 try_set_password(HostType, UserJID, Password, IQ, SubEl, Lang);
297 try_register_or_set_password(HostType, LUser, Server, Password, _From, IQ, SubEl, Source, Lang) ->
298 122 case check_timeout(Source) of
299 true ->
300 112 case try_register(HostType, LUser, Server, Password, Source, Lang) of
301 ok ->
302 108 IQ#iq{type = result, sub_el = [SubEl]};
303 {error, Error} ->
304 4 error_response(IQ, [SubEl, Error])
305 end;
306 false ->
307 10 ErrText = <<"Users are not allowed to register accounts so quickly">>,
308 10 error_response(IQ, mongoose_xmpp_errors:resource_constraint(Lang, ErrText))
309 end.
310
311 %% @doc Try to change password and return IQ response
312 try_set_password(HostType, #jid{} = UserJID, Password, IQ, SubEl, Lang) ->
313 4 case is_strong_password(HostType, Password) of
314 true ->
315 4 case ejabberd_auth:set_password(UserJID, Password) of
316 ok ->
317 2 IQ#iq{type = result, sub_el = [SubEl]};
318 {error, empty_password} ->
319 2 error_response(IQ, [SubEl, mongoose_xmpp_errors:bad_request()]);
320 {error, not_allowed} ->
321
:-(
error_response(IQ, [SubEl, mongoose_xmpp_errors:not_allowed()]);
322 {error, invalid_jid} ->
323
:-(
error_response(IQ, [SubEl, mongoose_xmpp_errors:item_not_found()])
324 end;
325 false ->
326
:-(
ErrText = <<"The password is too weak">>,
327
:-(
error_response(IQ, [SubEl, mongoose_xmpp_errors:not_acceptable(Lang, ErrText)])
328 end.
329
330 try_register(HostType, User, Server, Password, SourceRaw, Lang) ->
331 112 case jid:is_nodename(User) of
332 false ->
333
:-(
{error, mongoose_xmpp_errors:bad_request()};
334 _ ->
335 112 JID = jid:make(User, Server, <<>>),
336 112 Access = gen_mod:get_module_opt(HostType, ?MODULE, access),
337 112 IPAccess = get_ip_access(HostType),
338 112 case {acl:match_rule(HostType, Server, Access, JID),
339 check_ip_access(SourceRaw, IPAccess)} of
340 {deny, _} ->
341 1 {error, mongoose_xmpp_errors:forbidden()};
342 {_, deny} ->
343
:-(
{error, mongoose_xmpp_errors:forbidden()};
344 {allow, allow} ->
345 111 verify_password_and_register(HostType, JID, Password, SourceRaw, Lang)
346 end
347 end.
348
349 verify_password_and_register(HostType, #jid{} = JID, Password, SourceRaw, Lang) ->
350 111 case is_strong_password(HostType, Password) of
351 true ->
352 111 case ejabberd_auth:try_register(JID, Password) of
353 {error, exists} ->
354 2 {error, mongoose_xmpp_errors:conflict()};
355 {error, invalid_jid} ->
356
:-(
{error, mongoose_xmpp_errors:jid_malformed()};
357 {error, not_allowed} ->
358
:-(
{error, mongoose_xmpp_errors:not_allowed()};
359 {error, null_password} ->
360 1 {error, mongoose_xmpp_errors:not_acceptable()};
361 _ ->
362 108 send_welcome_message(HostType, JID),
363 108 send_registration_notifications(HostType, JID, SourceRaw),
364 108 ok
365 end;
366 false ->
367
:-(
ErrText = <<"The password is too weak">>,
368
:-(
{error, mongoose_xmpp_errors:not_acceptable(Lang, ErrText)}
369 end.
370
371 send_welcome_message(HostType, #jid{lserver = Server} = JID) ->
372 108 case gen_mod:lookup_module_opt(HostType, ?MODULE, welcome_message) of
373 {error, not_found} ->
374 108 ok;
375 {ok, {Subj, Body}} ->
376
:-(
ejabberd_router:route(
377 jid:make_noprep(<<>>, Server, <<>>),
378 JID,
379 #xmlel{name = <<"message">>, attrs = [{<<"type">>, <<"normal">>}],
380 children = [#xmlel{name = <<"subject">>,
381 children = [#xmlcdata{content = Subj}]},
382 #xmlel{name = <<"body">>,
383 children = [#xmlcdata{content = Body}]}]})
384 end.
385
386 send_registration_notifications(HostType, #jid{lserver = Domain} = UJID, Source) ->
387 108 case gen_mod:get_module_opt(HostType, ?MODULE, registration_watchers) of
388 106 [] -> 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 122 Timeout = mongoose_config:get_opt(registration_timeout),
416 122 case is_integer(Timeout) of
417 true ->
418 13 Priority = -(erlang:system_time(second)),
419 13 CleanPriority = Priority + Timeout,
420 13 F = fun() -> check_and_store_ip_entry(Source, Priority, CleanPriority) end,
421
422 13 case mnesia:transaction(F) of
423 {atomic, Res} ->
424 13 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 13 Treap = case mnesia:read(mod_register_ip, treap, write) of
436 [] ->
437 1 treap:empty();
438 12 [{mod_register_ip, treap, T}] -> T
439 end,
440 13 Treap1 = clean_treap(Treap, CleanPriority),
441 13 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 10 mnesia:write({mod_register_ip, treap, Treap1}),
449 10 false
450 end.
451
452 clean_treap(Treap, CleanPriority) ->
453 15 case treap:is_empty(Treap) of
454 true ->
455 3 Treap;
456 false ->
457 12 {_Key, Priority, _Value} = treap:get_root(Treap),
458 12 case Priority > CleanPriority of
459 2 true -> clean_treap(treap:delete_root(Treap), CleanPriority);
460 10 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) 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 764 jid:make(<<>>, Name, <<>>).
543
544 set_sender(#xmlel{attrs = A} = Stanza, #jid{} = From) ->
545 382 Stanza#xmlel{attrs = [{<<"from">>, jid:to_binary(From)}|A]}.
546
547 is_query_element(#xmlel{name = <<"query">>}) ->
548 498 true;
549 is_query_element(_) ->
550
:-(
false.
551
552 error_response(Request, Reasons) when is_list(Reasons) ->
553 6 Request#iq{type = error, sub_el = Reasons};
554 error_response(Request, Reason) ->
555 12 Request#iq{type = error, sub_el = Reason}.
Line Hits Source