./ct_report/coverage/mod_disco.COVER.html

1 %%%----------------------------------------------------------------------
2 %%% File : mod_disco.erl
3 %%% Author : Alexey Shchepin <alexey@process-one.net>
4 %%% Purpose : Service Discovery (XEP-0030) support
5 %%% Created : 1 Jan 2003 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_disco).
27 -author('alexey@process-one.net').
28 -xep([{xep, 30}, {version, "2.5rc3"}]).
29 -xep([{xep, 157}, {version, "1.1.1"}]).
30 -behaviour(gen_mod).
31 -behaviour(mongoose_module_metrics).
32
33 %% gen_mod callbacks
34 -export([start/2,
35 stop/1,
36 hooks/1,
37 config_spec/0,
38 supported_features/0]).
39
40 %% iq handlers
41 -export([process_local_iq_items/5,
42 process_local_iq_info/5,
43 process_sm_iq_items/5,
44 process_sm_iq_info/5]).
45
46 %% hook handlers
47 -export([disco_local_identity/3,
48 disco_sm_identity/3,
49 disco_local_items/3,
50 disco_sm_items/3,
51 disco_local_features/3,
52 disco_info/3]).
53
54 -ignore_xref([disco_info/1, disco_local_identity/1, disco_local_items/1,
55 disco_sm_identity/1, disco_sm_items/1]).
56
57 -include("mongoose.hrl").
58 -include("jlib.hrl").
59 -include("mongoose_config_spec.hrl").
60
61 -type return_hidden() :: mongoose_component:return_hidden().
62 -type server_info() :: #{name := binary(), urls := [binary()], modules => module()}.
63
64 -spec start(mongooseim:host_type(), gen_mod:module_opts()) -> ok.
65 start(HostType, #{iqdisc := IQDisc}) ->
66 421 [gen_iq_handler:add_iq_handler_for_domain(HostType, NS, Component, Handler, #{}, IQDisc) ||
67 421 {Component, NS, Handler} <- iq_handlers()],
68 421 ok.
69
70 -spec stop(mongooseim:host_type()) -> ok.
71 stop(HostType) ->
72 421 [gen_iq_handler:remove_iq_handler_for_domain(HostType, NS, Component) ||
73 421 {Component, NS, _Handler} <- iq_handlers()],
74 421 ok.
75
76 hooks(HostType) ->
77 842 [{disco_local_features, HostType, fun ?MODULE:disco_local_features/3, #{}, 100},
78 {disco_local_items, HostType, fun ?MODULE:disco_local_items/3, #{}, 100},
79 {disco_local_identity, HostType, fun ?MODULE:disco_local_identity/3, #{}, 100},
80 {disco_sm_items, HostType, fun ?MODULE:disco_sm_items/3, #{}, 100},
81 {disco_sm_identity, HostType, fun ?MODULE:disco_sm_identity/3, #{}, 100},
82 {disco_info, HostType, fun ?MODULE:disco_info/3, #{}, 100}].
83
84 iq_handlers() ->
85 842 [{ejabberd_local, ?NS_DISCO_ITEMS, fun ?MODULE:process_local_iq_items/5},
86 {ejabberd_local, ?NS_DISCO_INFO, fun ?MODULE:process_local_iq_info/5},
87 {ejabberd_sm, ?NS_DISCO_ITEMS, fun ?MODULE:process_sm_iq_items/5},
88 {ejabberd_sm, ?NS_DISCO_INFO, fun ?MODULE:process_sm_iq_info/5}].
89
90 -spec config_spec() -> mongoose_config_spec:config_section().
91 config_spec() ->
92 208 #section{
93 items = #{<<"extra_domains">> => #list{items = #option{type = binary,
94 validate = domain}},
95 <<"server_info">> => #list{items = server_info_spec()},
96 <<"users_can_see_hidden_services">> => #option{type = boolean},
97 <<"iqdisc">> => mongoose_config_spec:iqdisc()
98 },
99 defaults = #{<<"extra_domains">> => [],
100 <<"server_info">> => [],
101 <<"users_can_see_hidden_services">> => true,
102 <<"iqdisc">> => one_queue}
103 }.
104
105 server_info_spec() ->
106 208 #section{
107 items = #{<<"name">> => #option{type = binary,
108 validate = non_empty},
109 <<"urls">> => #list{items = #option{type = binary,
110 validate = url}},
111 <<"modules">> => #list{items = #option{type = atom,
112 validate = module}}
113 },
114 required = [<<"name">>, <<"urls">>]
115 }.
116
117 204 supported_features() -> [dynamic_domains].
118
119 %% IQ handlers
120
121 -spec process_local_iq_items(mongoose_acc:t(), jid:jid(), jid:jid(), jlib:iq(), map()) ->
122 {mongoose_acc:t(), jlib:iq()}.
123 process_local_iq_items(Acc, _From, _To, #iq{type = set, sub_el = SubEl} = IQ, _Extra) ->
124
:-(
{Acc, IQ#iq{type = error, sub_el = [SubEl, mongoose_xmpp_errors:not_allowed()]}};
125 process_local_iq_items(Acc, From, To, #iq{type = get, lang = Lang, sub_el = SubEl} = IQ, _Extra) ->
126 45 HostType = mongoose_acc:host_type(Acc),
127 45 Node = xml:get_tag_attr_s(<<"node">>, SubEl),
128 45 case mongoose_disco:get_local_items(HostType, From, To, Node, Lang) of
129 empty ->
130
:-(
Error = mongoose_xmpp_errors:item_not_found(),
131
:-(
{Acc, IQ#iq{type = error, sub_el = [SubEl, Error]}};
132 {result, ItemsXML} ->
133 45 {Acc, make_iq_result(IQ, ?NS_DISCO_ITEMS, Node, ItemsXML)}
134 end.
135
136 -spec process_local_iq_info(mongoose_acc:t(), jid:jid(), jid:jid(), jlib:iq(), map()) ->
137 {mongoose_acc:t(), jlib:iq()}.
138 process_local_iq_info(Acc, _From, _To, #iq{type = set, sub_el = SubEl} = IQ, _Extra) ->
139
:-(
{Acc, IQ#iq{type = error, sub_el = [SubEl, mongoose_xmpp_errors:not_allowed()]}};
140 process_local_iq_info(Acc, From, To, #iq{type = get, lang = Lang, sub_el = SubEl} = IQ, _Extra) ->
141 59 HostType = mongoose_acc:host_type(Acc),
142 59 Node = xml:get_tag_attr_s(<<"node">>, SubEl),
143 59 case mongoose_disco:get_local_features(HostType, From, To, Node, Lang) of
144 empty ->
145
:-(
Error = mongoose_xmpp_errors:item_not_found(),
146
:-(
{Acc, IQ#iq{type = error, sub_el = [SubEl, Error]}};
147 {result, FeaturesXML} ->
148 59 IdentityXML = mongoose_disco:get_local_identity(HostType, From, To, Node, Lang),
149 59 InfoXML = mongoose_disco:get_info(HostType, ?MODULE, Node, Lang),
150 59 {Acc, make_iq_result(IQ, ?NS_DISCO_INFO, Node, IdentityXML ++ InfoXML ++ FeaturesXML)}
151 end.
152
153 -spec process_sm_iq_items(mongoose_acc:t(), jid:jid(), jid:jid(), jlib:iq(), map()) ->
154 {mongoose_acc:t(), jlib:iq()}.
155 process_sm_iq_items(Acc, _From, _To, #iq{type = set, sub_el = SubEl} = IQ, _Extra) ->
156
:-(
{Acc, IQ#iq{type = error, sub_el = [SubEl, mongoose_xmpp_errors:not_allowed()]}};
157 process_sm_iq_items(Acc, From, To, #iq{type = get, lang = Lang, sub_el = SubEl} = IQ, _Extra) ->
158 16 case is_presence_subscribed(From, To) of
159 true ->
160 13 HostType = mongoose_acc:host_type(Acc),
161 13 Node = xml:get_tag_attr_s(<<"node">>, SubEl),
162 13 case mongoose_disco:get_sm_items(HostType, From, To, Node, Lang) of
163 empty ->
164 6 Error = sm_error(From, To),
165 6 {Acc, IQ#iq{type = error, sub_el = [SubEl, Error]}};
166 {result, ItemsXML} ->
167 7 {Acc, make_iq_result(IQ, ?NS_DISCO_ITEMS, Node, ItemsXML)}
168 end;
169 false ->
170 3 {Acc, IQ#iq{type = error, sub_el = [SubEl, mongoose_xmpp_errors:service_unavailable()]}}
171 end.
172
173 -spec process_sm_iq_info(mongoose_acc:t(), jid:jid(), jid:jid(), jlib:iq(), map()) ->
174 {mongoose_acc:t(), jlib:iq()}.
175 process_sm_iq_info(Acc, _From, _To, #iq{type = set, sub_el = SubEl} = IQ, _Extra) ->
176
:-(
{Acc, IQ#iq{type = error, sub_el = [SubEl, mongoose_xmpp_errors:not_allowed()]}};
177 process_sm_iq_info(Acc, From, To, #iq{type = get, lang = Lang, sub_el = SubEl} = IQ, _Extra) ->
178 15 case is_presence_subscribed(From, To) of
179 true ->
180 12 HostType = mongoose_acc:host_type(Acc),
181 12 Node = xml:get_tag_attr_s(<<"node">>, SubEl),
182 12 case mongoose_disco:get_sm_features(HostType, From, To, Node, Lang) of
183 empty ->
184
:-(
Error = sm_error(From, To),
185
:-(
{Acc, IQ#iq{type = error, sub_el = [SubEl, Error]}};
186 {result, FeaturesXML} ->
187 12 IdentityXML = mongoose_disco:get_sm_identity(HostType, From, To, Node, Lang),
188 12 {Acc, make_iq_result(IQ, ?NS_DISCO_INFO, Node, IdentityXML ++ FeaturesXML)}
189 end;
190 false ->
191 3 {Acc, IQ#iq{type = error, sub_el = [SubEl, mongoose_xmpp_errors:service_unavailable()]}}
192 end.
193
194 %% Hook handlers
195
196 -spec disco_local_identity(Acc, Params, Extra) -> {ok, Acc} when
197 Acc :: mongoose_disco:identity_acc(),
198 Params :: map(),
199 Extra :: map().
200 disco_local_identity(Acc = #{node := <<>>}, _, _) ->
201 222 {ok, mongoose_disco:add_identities([#{category => <<"server">>,
202 type => <<"im">>,
203 name => <<"MongooseIM">>}], Acc)};
204 disco_local_identity(Acc, _, _) ->
205 5 {ok, Acc}.
206
207 -spec disco_sm_identity(Acc, Params, Extra) -> {ok, Acc} when
208 Acc :: mongoose_disco:identity_acc(),
209 Params :: map(),
210 Extra :: map().
211 disco_sm_identity(Acc = #{to_jid := JID}, _, _) ->
212 12 NewAcc = case ejabberd_auth:does_user_exist(JID) of
213 12 true -> mongoose_disco:add_identities([#{category => <<"account">>,
214 type => <<"registered">>}], Acc);
215
:-(
false -> Acc
216 end,
217 12 {ok, NewAcc}.
218
219 -spec disco_local_items(Acc, Params, Extra) -> {ok, Acc} when
220 Acc :: mongoose_disco:item_acc(),
221 Params :: map(),
222 Extra :: map().
223 disco_local_items(Acc = #{host_type := HostType, from_jid := From, to_jid := To, node := <<>>}, _, _) ->
224 43 ReturnHidden = should_return_hidden(HostType, From),
225 43 Subdomains = get_subdomains(To#jid.lserver),
226 43 Components = get_external_components(To#jid.lserver, ReturnHidden),
227 43 ExtraDomains = get_extra_domains(HostType),
228 43 Domains = Subdomains ++ Components ++ ExtraDomains,
229 43 {ok, mongoose_disco:add_items([#{jid => Domain} || Domain <- Domains], Acc)};
230 disco_local_items(Acc, _, _) ->
231 2 {ok, Acc}.
232
233 -spec disco_sm_items(Acc, Params, Extra) -> {ok, Acc} when
234 Acc :: mongoose_disco:item_acc(),
235 Params :: map(),
236 Extra :: map().
237 disco_sm_items(Acc = #{to_jid := To, node := <<>>}, _, _) ->
238 7 Items = get_user_resources(To),
239 7 {ok, mongoose_disco:add_items(Items, Acc)};
240 disco_sm_items(Acc, _, _) ->
241 6 {ok, Acc}.
242
243 -spec disco_local_features(Acc, Params, Extra) -> {ok, Acc} when
244 Acc :: mongoose_disco:feature_acc(),
245 Params :: map(),
246 Extra :: map().
247 disco_local_features(Acc = #{node := <<>>}, _, _) ->
248 222 {ok, mongoose_disco:add_features([<<"iq">>, <<"presence">>, <<"presence-invisible">>], Acc)};
249 disco_local_features(Acc, _, _) ->
250 5 {ok, Acc}.
251
252 %% @doc Support for: XEP-0157 Contact Addresses for XMPP Services
253 -spec disco_info(Acc, Params, Extra) -> {ok, Acc} when
254 Acc :: mongoose_disco:info_acc(),
255 Params :: map(),
256 Extra :: map().
257 disco_info(Acc = #{host_type := HostType, module := Module, node := <<>>}, _, _) ->
258 243 ServerInfoList = gen_mod:get_module_opt(HostType, ?MODULE, server_info),
259 243 Fields = [server_info_to_field(ServerInfo) || ServerInfo <- ServerInfoList,
260 108 is_module_allowed(Module, ServerInfo)],
261 243 {ok, mongoose_disco:add_info([#{xmlns => ?NS_SERVERINFO, fields => Fields}], Acc)};
262 disco_info(Acc, _, _) ->
263 5 {ok, Acc}.
264
265 -spec get_extra_domains(mongooseim:host_type()) -> [jid:lserver()].
266 get_extra_domains(HostType) ->
267 43 gen_mod:get_module_opt(HostType, ?MODULE, extra_domains).
268
269 %% Internal functions
270
271 -spec should_return_hidden(mongooseim:host_type(), From :: jid:jid()) -> return_hidden().
272 should_return_hidden(_HostType, #jid{ luser = <<>> } = _From) ->
273 %% We respect "is hidden" flag only when a client performs the query
274
:-(
all;
275 should_return_hidden(HostType, _From) ->
276 43 case gen_mod:get_module_opt(HostType, ?MODULE, users_can_see_hidden_services) of
277 3 true -> all;
278 40 false -> only_public
279 end.
280
281 -spec get_subdomains(jid:lserver()) -> [jid:lserver()].
282 get_subdomains(Domain) ->
283 43 [maps:get(subdomain, SubdomainInfo) ||
284 43 SubdomainInfo <- mongoose_domain_api:get_all_subdomains_for_domain(Domain)].
285
286 %% TODO: This code can be removed when components register subdomains in the domain API.
287 %% Until then, it works only for static domains.
288 -spec get_external_components(jid:server(), return_hidden()) -> [jid:lserver()].
289 get_external_components(Domain, ReturnHidden) ->
290 43 StaticDomains = lists:sort(fun(H1, H2) -> size(H1) >= size(H2) end, ?MYHOSTS),
291 43 lists:filter(
292 fun(Component) ->
293 29 check_if_host_is_the_shortest_suffix_for_route(Component, Domain, StaticDomains)
294 end, mongoose_component:dirty_get_all_components(ReturnHidden)).
295
296 -spec check_if_host_is_the_shortest_suffix_for_route(
297 Route :: jid:lserver(), Host :: jid:lserver(), VHosts :: [jid:lserver()]) -> boolean().
298 check_if_host_is_the_shortest_suffix_for_route(Route, Host, VHosts) ->
299 29 RouteS = binary_to_list(Route),
300 29 case lists:dropwhile(
301 fun(VH) ->
302 81 not lists:suffix("." ++ binary_to_list(VH), RouteS)
303 end, VHosts) of
304 [] ->
305
:-(
false;
306 [VH | _] ->
307 29 VH == Host
308 end.
309
310 %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
311
312 -spec is_presence_subscribed(jid:jid(), jid:jid()) -> boolean().
313 is_presence_subscribed(#jid{luser = LFromUser, lserver = LFromServer} = FromJID,
314 #jid{luser = LToUser, lserver = LToServer} = _To) ->
315 31 {ok, HostType} = mongoose_domain_api:get_domain_host_type(LFromServer),
316 31 A = mongoose_acc:new(#{ location => ?LOCATION,
317 host_type => HostType,
318 lserver => LFromServer,
319 element => undefined }),
320 31 A2 = mongoose_hooks:roster_get(A, FromJID),
321 31 Roster = mongoose_acc:get(roster, items, [], A2),
322 31 lists:any(fun({roster, _, _, JID, _, S, _, _, _, _}) ->
323 10 {TUser, TServer} = jid:to_lus(JID),
324 10 LToUser == TUser andalso LToServer == TServer andalso S /= none
325 end,
326 Roster)
327 22 orelse LFromUser == LToUser andalso LFromServer == LToServer.
328
329 sm_error(#jid{luser = LUser, lserver = LServer},
330 #jid{luser = LUser, lserver = LServer}) ->
331 3 mongoose_xmpp_errors:item_not_found();
332 sm_error(_From, _To) ->
333 3 mongoose_xmpp_errors:not_allowed().
334
335 -spec get_user_resources(jid:jid()) -> [mongoose_disco:item()].
336 get_user_resources(JID = #jid{luser = LUser}) ->
337 7 Rs = ejabberd_sm:get_user_resources(JID),
338 7 lists:map(fun(R) ->
339 7 BJID = jid:to_binary(jid:replace_resource_noprep(JID, R)),
340 7 #{jid => BJID, name => LUser}
341 end, lists:sort(Rs)).
342
343 %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
344
345 -spec make_iq_result(jlib:iq(), binary(), binary(), [exml:element()]) -> jlib:iq().
346 make_iq_result(IQ, NameSpace, Node, ChildrenXML) ->
347 123 IQ#iq{type = result,
348 sub_el = [#xmlel{name = <<"query">>,
349 attrs = [{<<"xmlns">>, NameSpace} | make_node_attrs(Node)],
350 children = ChildrenXML
351 }]}.
352
353 -spec make_node_attrs(Node :: binary()) -> [{binary(), binary()}].
354 112 make_node_attrs(<<>>) -> [];
355 11 make_node_attrs(Node) -> [{<<"node">>, Node}].
356
357 -spec server_info_to_field(server_info()) -> mongoose_disco:info_field().
358 server_info_to_field(#{name := Name, urls := URLs}) ->
359 72 #{var => Name, values => URLs}.
360
361 -spec is_module_allowed(module(), server_info()) -> boolean().
362 72 is_module_allowed(Module, #{modules := Modules}) -> lists:member(Module, Modules);
363 36 is_module_allowed(_Module, #{}) -> true.
Line Hits Source