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