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