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