1 |
|
%%%---------------------------------------------------------------------- |
2 |
|
%%% File : mod_caps.erl |
3 |
|
%%% Author : Magnus Henoch <henoch@dtek.chalmers.se> |
4 |
|
%%% Purpose : Request and cache Entity Capabilities (XEP-0115) |
5 |
|
%%% Created : 7 Oct 2006 by Magnus Henoch <henoch@dtek.chalmers.se> |
6 |
|
%%% |
7 |
|
%%% |
8 |
|
%%% ejabberd, Copyright (C) 2002-2015 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 along |
21 |
|
%%% with this program; if not, write to the Free Software Foundation, Inc., |
22 |
|
%%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. |
23 |
|
%%% |
24 |
|
%%% 2009, improvements from ProcessOne to support correct PEP handling |
25 |
|
%%% through s2s, use less memory, and speedup global caps handling |
26 |
|
%%%---------------------------------------------------------------------- |
27 |
|
|
28 |
|
-module(mod_caps). |
29 |
|
|
30 |
|
-author('henoch@dtek.chalmers.se'). |
31 |
|
|
32 |
|
-xep([{xep, 115}, {version, "1.6.0"}]). |
33 |
|
|
34 |
|
-behaviour(gen_server). |
35 |
|
-behaviour(gen_mod). |
36 |
|
-behaviour(mongoose_module_metrics). |
37 |
|
|
38 |
|
-export([read_caps/1, caps_stream_features/3, |
39 |
|
disco_local_features/3, disco_local_identity/3, disco_info/3]). |
40 |
|
|
41 |
|
%% gen_mod callbacks |
42 |
|
-export([start/2, start_link/2, stop/1, config_spec/0, supported_features/0]). |
43 |
|
|
44 |
|
%% gen_server callbacks |
45 |
|
-export([init/1, handle_info/2, handle_call/3, |
46 |
|
handle_cast/2, terminate/2, code_change/3]). |
47 |
|
|
48 |
|
-export([user_send_presence/3, |
49 |
|
user_receive_presence/3, |
50 |
|
get_pep_recipients/3, |
51 |
|
filter_pep_recipient/3]). |
52 |
|
|
53 |
|
%% for test cases |
54 |
|
-export([delete_caps/2, make_disco_hash/2]). |
55 |
|
-ignore_xref([delete_caps/2, make_disco_hash/2, read_caps/1, start_link/2]). |
56 |
|
|
57 |
|
-include("mongoose.hrl"). |
58 |
|
-include("mongoose_config_spec.hrl"). |
59 |
|
|
60 |
|
-include("jlib.hrl"). |
61 |
|
|
62 |
|
-define(PROCNAME, ejabberd_mod_caps). |
63 |
|
|
64 |
|
-define(BAD_HASH_LIFETIME, 600). |
65 |
|
|
66 |
|
-record(caps, |
67 |
|
{ |
68 |
|
node = <<>> :: binary(), |
69 |
|
version = <<>> :: binary(), |
70 |
|
hash = <<>> :: binary(), |
71 |
|
exts = [] :: [binary()] |
72 |
|
}). |
73 |
|
|
74 |
|
-type caps() :: #caps{}. |
75 |
|
-type caps_resources() :: gb_trees:tree(jid:simple_jid(), caps()). |
76 |
|
|
77 |
|
-export_type([caps/0, node_pair/0, maybe_pending_features/0]). |
78 |
|
|
79 |
|
-type features() :: [binary()]. |
80 |
|
-type maybe_pending_features() :: features() | pos_integer(). |
81 |
|
-type node_pair() :: {Node :: binary(), SubNode :: binary()}. |
82 |
|
|
83 |
|
-record(state, {host_type :: mongooseim:host_type()}). |
84 |
|
|
85 |
|
-type state() :: #state{}. |
86 |
|
|
87 |
|
-spec start_link(mongooseim:host_type(), gen_mod:module_opts()) -> any(). |
88 |
|
start_link(HostType, Opts) -> |
89 |
2 |
mod_caps_backend:init(HostType, Opts), |
90 |
2 |
Proc = gen_mod:get_module_proc(HostType, ?PROCNAME), |
91 |
2 |
gen_server:start_link({local, Proc}, ?MODULE, |
92 |
|
[HostType, Opts], []). |
93 |
|
|
94 |
|
-spec start(mongooseim:host_type(), gen_mod:module_opts()) -> any(). |
95 |
|
start(HostType, Opts) -> |
96 |
2 |
Proc = gen_mod:get_module_proc(HostType, ?PROCNAME), |
97 |
2 |
ChildSpec = {Proc, {?MODULE, start_link, [HostType, Opts]}, |
98 |
|
transient, 1000, worker, [?MODULE]}, |
99 |
2 |
ejabberd_sup:start_child(ChildSpec). |
100 |
|
|
101 |
|
-spec stop(mongooseim:host_type()) -> any(). |
102 |
|
stop(HostType) -> |
103 |
2 |
Proc = gen_mod:get_module_proc(HostType, ?PROCNAME), |
104 |
2 |
gen_server:call(Proc, stop), |
105 |
2 |
ejabberd_sup:stop_child(Proc). |
106 |
|
|
107 |
|
-spec config_spec() -> mongoose_config_spec:config_section(). |
108 |
|
config_spec() -> |
109 |
84 |
#section{ |
110 |
|
items = #{<<"cache_size">> => #option{type = integer, |
111 |
|
validate = positive}, |
112 |
|
<<"cache_life_time">> => #option{type = integer, |
113 |
|
validate = positive}, |
114 |
|
<<"backend">> => #option{type = atom, |
115 |
|
validate = {module, ?MODULE}} |
116 |
|
}, |
117 |
|
defaults = #{<<"cache_size">> => 1000, |
118 |
|
<<"cache_life_time">> => timer:hours(24) div 1000, |
119 |
|
<<"backend">> => mnesia} |
120 |
|
}. |
121 |
|
|
122 |
2 |
supported_features() -> [dynamic_domains]. |
123 |
|
|
124 |
|
-spec get_features_list(mongooseim:host_type(), nothing | caps()) -> features(). |
125 |
|
get_features_list(HostType, Caps) -> |
126 |
:-( |
case get_features(HostType, Caps) of |
127 |
:-( |
unknown -> []; |
128 |
:-( |
Features -> Features |
129 |
|
end. |
130 |
|
|
131 |
|
-spec get_features(mongooseim:host_type(), nothing | caps()) -> unknown | features(). |
132 |
:-( |
get_features(_HostType, nothing) -> []; |
133 |
|
get_features(HostType, #caps{node = Node, version = Version, exts = Exts}) -> |
134 |
:-( |
SubNodes = [Version | Exts], |
135 |
:-( |
lists:foldl(fun (SubNode, Acc) -> |
136 |
:-( |
NodePair = {Node, SubNode}, |
137 |
:-( |
case cache_tab:lookup(caps_features, NodePair, |
138 |
|
caps_read_fun(HostType, NodePair)) |
139 |
|
of |
140 |
|
{ok, Features} when is_list(Features) -> |
141 |
:-( |
Features ++ Acc; |
142 |
|
_ when Acc == [] -> |
143 |
:-( |
unknown; |
144 |
|
_ -> |
145 |
:-( |
Acc |
146 |
|
end |
147 |
|
end, |
148 |
|
[], SubNodes). |
149 |
|
|
150 |
|
-spec read_caps([exml:element()]) -> nothing | caps(). |
151 |
69 |
read_caps(Els) -> read_caps(Els, nothing). |
152 |
|
|
153 |
|
read_caps([#xmlel{name = <<"c">>, attrs = Attrs} | Tail], Result) -> |
154 |
:-( |
case xml:get_attr_s(<<"xmlns">>, Attrs) of |
155 |
|
?NS_CAPS -> |
156 |
:-( |
Node = xml:get_attr_s(<<"node">>, Attrs), |
157 |
:-( |
Version = xml:get_attr_s(<<"ver">>, Attrs), |
158 |
:-( |
Hash = xml:get_attr_s(<<"hash">>, Attrs), |
159 |
:-( |
Exts = mongoose_bin:tokens(xml:get_attr_s(<<"ext">>, Attrs), <<" ">>), |
160 |
:-( |
read_caps(Tail, #caps{node = Node, hash = Hash, version = Version, exts = Exts}); |
161 |
:-( |
_ -> read_caps(Tail, Result) |
162 |
|
end; |
163 |
|
read_caps([#xmlel{name = <<"x">>, attrs = Attrs} | Tail], Result) -> |
164 |
:-( |
case xml:get_attr_s(<<"xmlns">>, Attrs) of |
165 |
:-( |
?NS_MUC_USER -> nothing; |
166 |
:-( |
_ -> read_caps(Tail, Result) |
167 |
|
end; |
168 |
|
read_caps([_ | Tail], Result) -> |
169 |
5 |
read_caps(Tail, Result); |
170 |
69 |
read_caps([], Result) -> Result. |
171 |
|
|
172 |
|
-spec user_send_presence(Acc, Params, Extra) -> {ok, Acc} when |
173 |
|
Acc :: mongoose_acc:t(), |
174 |
|
Params :: map(), |
175 |
|
Extra :: map(). |
176 |
|
user_send_presence(Acc, _, _) -> |
177 |
50 |
{From, To, Packet} = mongoose_acc:packet(Acc), |
178 |
50 |
{ok, user_send_presence(Acc, From, To, Packet)}. |
179 |
|
|
180 |
|
-spec user_send_presence(mongoose_acc:t(), jid:jid(), jid:jid(), exml:element()) -> mongoose_acc:t(). |
181 |
|
user_send_presence(Acc, |
182 |
|
#jid{luser = User, lserver = LServer} = From, |
183 |
|
#jid{luser = User, lserver = LServer, lresource = <<>>}, |
184 |
|
#xmlel{attrs = Attrs, children = Elements}) -> |
185 |
26 |
Type = xml:get_attr_s(<<"type">>, Attrs), |
186 |
26 |
handle_presence(Acc, LServer, From, Type, Elements); |
187 |
|
user_send_presence(Acc, _, _, _) -> |
188 |
24 |
Acc. |
189 |
|
|
190 |
|
-spec caps_stream_features(Acc, Params, Extra) -> {ok, Acc} when |
191 |
|
Acc :: [exml:element()], |
192 |
|
Params :: #{lserver := jid:lserver()}, |
193 |
|
Extra :: #{host_type := mongooseim:host_type()}. |
194 |
|
caps_stream_features(Acc, #{lserver := LServer}, #{host_type := HostType}) -> |
195 |
58 |
NewAcc = case make_my_disco_hash(HostType, LServer) of |
196 |
|
<<>> -> |
197 |
:-( |
Acc; |
198 |
|
Hash -> |
199 |
58 |
[#xmlel{name = <<"c">>, |
200 |
|
attrs = [{<<"xmlns">>, ?NS_CAPS}, {<<"hash">>, <<"sha-1">>}, |
201 |
|
{<<"node">>, ?MONGOOSE_URI}, {<<"ver">>, Hash}], |
202 |
|
children = []} |
203 |
|
| Acc] |
204 |
|
end, |
205 |
58 |
{ok, NewAcc}. |
206 |
|
|
207 |
|
-spec disco_local_features(mongoose_disco:feature_acc(), |
208 |
|
map(), |
209 |
|
map()) -> {ok, mongoose_disco:feature_acc()}. |
210 |
|
disco_local_features(Acc = #{node := Node}, _, _) -> |
211 |
63 |
NewAcc = case is_valid_node(Node) of |
212 |
2 |
true -> Acc#{node := <<>>}; |
213 |
61 |
false -> Acc |
214 |
|
end, |
215 |
63 |
{ok, NewAcc}. |
216 |
|
|
217 |
|
-spec disco_local_identity(Acc, Params, Extra) -> {ok, Acc} when |
218 |
|
Acc :: mongoose_disco:identity_acc(), |
219 |
|
Params :: map(), |
220 |
|
Extra :: gen_hook:extra(). |
221 |
|
disco_local_identity(Acc = #{node := Node}, _, _) -> |
222 |
63 |
NewAcc = case is_valid_node(Node) of |
223 |
2 |
true -> Acc#{node := <<>>}; |
224 |
61 |
false -> Acc |
225 |
|
end, |
226 |
63 |
{ok, NewAcc}. |
227 |
|
|
228 |
|
-spec disco_info(Acc, Params, Extra) -> {ok, Acc} when |
229 |
|
Acc :: mongoose_disco:identity_acc(), |
230 |
|
Params :: map(), |
231 |
|
Extra :: gen_hook:extra(). |
232 |
|
disco_info(Acc = #{node := Node}, _, _) -> |
233 |
63 |
NewAcc = case is_valid_node(Node) of |
234 |
2 |
true -> Acc#{node := <<>>}; |
235 |
61 |
false -> Acc |
236 |
|
end, |
237 |
63 |
{ok, NewAcc}. |
238 |
|
|
239 |
|
-spec handle_presence(mongoose_acc:t(), jid:lserver(), jid:jid(), binary(), [exml:element()]) -> |
240 |
|
mongoose_acc:t(). |
241 |
|
handle_presence(Acc, LServer, From, Type, Elements) when Type =:= <<>>; |
242 |
|
Type =:= <<"available">> -> |
243 |
26 |
case read_caps(Elements) of |
244 |
|
nothing -> |
245 |
26 |
Acc; |
246 |
|
#caps{version = Version, exts = Exts} = Caps -> |
247 |
:-( |
feature_request(Acc, LServer, From, Caps, [Version | Exts]) |
248 |
|
end; |
249 |
|
handle_presence(Acc, _LServer, _From, _Type, _Elements) -> |
250 |
:-( |
Acc. |
251 |
|
|
252 |
|
-spec user_receive_presence(mongoose_acc:t(), mongoose_c2s_hooks:params(), gen_hook:extra()) -> |
253 |
|
mongoose_c2s_hooks:result(). |
254 |
|
user_receive_presence(Acc0, #{c2s_data := C2SData}, _Extra) -> |
255 |
93 |
{From, To, #xmlel{attrs = Attrs, children = Els} = Packet} = mongoose_acc:packet(Acc0), |
256 |
93 |
?LOG_DEBUG(#{what => user_receive_presence, |
257 |
|
to => jid:to_binary(To), from => jid:to_binary(From), |
258 |
93 |
exml_packet => Packet, c2s_state => C2SData}), |
259 |
93 |
Type = xml:get_attr_s(<<"type">>, Attrs), |
260 |
93 |
#jid{lserver = LServer} = mongoose_c2s:get_jid(C2SData), |
261 |
93 |
Acc = case mongoose_domain_api:get_host_type(From#jid.lserver) of |
262 |
|
{error, not_found} -> |
263 |
:-( |
handle_presence(Acc0, LServer, From, Type, Els); |
264 |
|
{ok, _} -> |
265 |
93 |
Acc0 %% it was already handled in 'user_send_presence' |
266 |
|
end, |
267 |
93 |
case mongoose_c2s:get_mod_state(C2SData, mod_presence) of |
268 |
|
{ok, Presences} -> |
269 |
93 |
Subscription = get_subscription(From, Presences), |
270 |
93 |
Insert = (Type == <<>> orelse Type == <<"available">>) |
271 |
93 |
and (Subscription == both orelse Subscription == to), |
272 |
93 |
Delete = Type == <<"unavailable">> orelse Type == <<"error">>, |
273 |
93 |
case Insert orelse Delete of |
274 |
|
true -> |
275 |
43 |
LFrom = jid:to_lower(From), |
276 |
43 |
Rs = case mongoose_c2s:get_mod_state(C2SData, ?MODULE) of |
277 |
17 |
{ok, Rs1} -> Rs1; |
278 |
26 |
{error, not_found} -> gb_trees:empty() |
279 |
|
end, |
280 |
43 |
Caps = read_caps(Els), |
281 |
43 |
NewRs = case Caps of |
282 |
|
nothing when Insert == true -> |
283 |
38 |
Rs; |
284 |
|
_ when Insert == true -> |
285 |
:-( |
?LOG_DEBUG(#{what => caps_set_caps, |
286 |
|
caps => Caps, |
287 |
|
to => jid:to_binary(To), |
288 |
|
from => jid:to_binary(From), |
289 |
|
exml_packet => Packet, |
290 |
:-( |
c2s_state => C2SData}), |
291 |
:-( |
upsert_caps(LFrom, Caps, Rs); |
292 |
|
_ -> |
293 |
5 |
gb_trees:delete_any(LFrom, Rs) |
294 |
|
end, |
295 |
43 |
{ok, mongoose_c2s_acc:to_acc(Acc, state_mod, {?MODULE, NewRs})}; |
296 |
|
false -> |
297 |
50 |
{ok, Acc} |
298 |
|
end; |
299 |
|
{error, not_found} -> |
300 |
:-( |
{ok, Acc} |
301 |
|
end. |
302 |
|
|
303 |
|
get_subscription(From, Presences) -> |
304 |
93 |
BareFrom = jid:to_bare(From), |
305 |
93 |
F = mod_presence:is_subscribed_to_my_presence(From, BareFrom, Presences), |
306 |
93 |
T = mod_presence:am_i_subscribed_to_presence(From, BareFrom, Presences), |
307 |
93 |
case {F, T} of |
308 |
69 |
{true, true} -> both; |
309 |
:-( |
{true, false} -> from; |
310 |
12 |
{false, true} -> to; |
311 |
12 |
{false, false} -> none |
312 |
|
end. |
313 |
|
|
314 |
|
-spec upsert_caps(jid:simple_jid(), caps(), caps_resources()) -> caps_resources(). |
315 |
|
upsert_caps(LFrom, Caps, Rs) -> |
316 |
:-( |
case gb_trees:lookup(LFrom, Rs) of |
317 |
:-( |
{value, Caps} -> Rs; |
318 |
|
none -> |
319 |
:-( |
gb_trees:insert(LFrom, Caps, Rs); |
320 |
|
_ -> |
321 |
:-( |
gb_trees:update(LFrom, Caps, Rs) |
322 |
|
end. |
323 |
|
|
324 |
|
-spec get_pep_recipients(Acc, Params, Extra) -> {ok, Acc} when |
325 |
|
Acc :: [jid:simple_jid()], |
326 |
|
Params :: #{c2s_data := mongoose_c2s:data(), feature := binary()}, |
327 |
|
Extra :: map(). |
328 |
|
get_pep_recipients(InAcc, #{c2s_data := C2SData, feature := Feature}, _) -> |
329 |
:-( |
HostType = mongoose_c2s:get_host_type(C2SData), |
330 |
:-( |
NewAcc = case mongoose_c2s:get_mod_state(C2SData, ?MODULE) of |
331 |
|
{ok, Rs} -> |
332 |
:-( |
filter_recipients_by_caps(HostType, InAcc, Feature, Rs); |
333 |
:-( |
_ -> InAcc |
334 |
|
end, |
335 |
:-( |
{ok, NewAcc}; |
336 |
:-( |
get_pep_recipients(Acc, _, _) -> {ok, Acc}. |
337 |
|
|
338 |
|
-spec filter_recipients_by_caps(mongooseim:host_type(), Acc, binary(), caps_resources()) -> Acc |
339 |
|
when Acc :: [jid:simple_jid()]. |
340 |
|
filter_recipients_by_caps(HostType, InAcc, Feature, Rs) -> |
341 |
:-( |
gb_trees_fold(fun(USR, Caps, Acc) -> |
342 |
:-( |
case lists:member(Feature, get_features_list(HostType, Caps)) of |
343 |
:-( |
true -> [USR | Acc]; |
344 |
:-( |
false -> Acc |
345 |
|
end |
346 |
|
end, |
347 |
|
InAcc, Rs). |
348 |
|
|
349 |
|
-spec filter_pep_recipient(Acc, Params, Extra) -> {ok | stop, Acc} when |
350 |
|
Acc :: boolean(), |
351 |
|
Params :: #{c2s_data := mongoose_c2s:data(), feature := binary(), to := jid:jid()}, |
352 |
|
Extra :: gen_hook:extra(). |
353 |
|
filter_pep_recipient(InAcc, #{c2s_data := C2SData, feature := Feature, to := To}, _) -> |
354 |
:-( |
case mongoose_c2s:get_mod_state(C2SData, ?MODULE) of |
355 |
|
{ok, Rs} -> |
356 |
:-( |
?LOG_DEBUG(#{what => caps_lookup, text => <<"Look for CAPS for To jid">>, |
357 |
:-( |
acc => InAcc, c2s_state => C2SData, caps_resources => Rs}), |
358 |
:-( |
LTo = jid:to_lower(To), |
359 |
:-( |
case gb_trees:lookup(LTo, Rs) of |
360 |
|
{value, Caps} -> |
361 |
:-( |
HostType = mongoose_c2s:get_host_type(C2SData), |
362 |
:-( |
Drop = not lists:member(Feature, get_features_list(HostType, Caps)), |
363 |
:-( |
{stop, Drop}; |
364 |
|
none -> |
365 |
:-( |
{stop, true} |
366 |
|
end; |
367 |
:-( |
_ -> {ok, InAcc} |
368 |
|
end. |
369 |
|
|
370 |
|
-spec init(list()) -> {ok, state()}. |
371 |
|
init([HostType, #{cache_size := MaxSize, cache_life_time := LifeTime}]) -> |
372 |
2 |
cache_tab:new(caps_features, [{max_size, MaxSize}, {life_time, LifeTime}]), |
373 |
2 |
gen_hook:add_handlers(hooks(HostType)), |
374 |
2 |
{ok, #state{host_type = HostType}}. |
375 |
|
|
376 |
|
-spec handle_call(term(), any(), state()) -> |
377 |
|
{stop, normal, ok, state()} | {reply, {error, any()}, state()}. |
378 |
|
handle_call(stop, _From, State) -> |
379 |
2 |
{stop, normal, ok, State}; |
380 |
|
handle_call(_Req, _From, State) -> |
381 |
:-( |
{reply, {error, badarg}, State}. |
382 |
|
|
383 |
|
-spec handle_cast(any(), state()) -> {noreply, state()}. |
384 |
:-( |
handle_cast(_Msg, State) -> {noreply, State}. |
385 |
|
|
386 |
|
-spec handle_info(any(), state()) -> {noreply, state()}. |
387 |
:-( |
handle_info(_Info, State) -> {noreply, State}. |
388 |
|
|
389 |
|
-spec terminate(any(), state()) -> ok. |
390 |
|
terminate(_Reason, #state{host_type = HostType}) -> |
391 |
2 |
gen_hook:delete_handlers(hooks(HostType)). |
392 |
|
|
393 |
|
hooks(HostType) -> |
394 |
4 |
[{disco_local_features, HostType, fun ?MODULE:disco_local_features/3, #{}, 1}, |
395 |
|
{get_pep_recipients, HostType, fun ?MODULE:get_pep_recipients/3, #{}, 75}, |
396 |
|
{filter_pep_recipient, HostType, fun ?MODULE:filter_pep_recipient/3, #{}, 75}, |
397 |
|
{user_send_presence, HostType, fun ?MODULE:user_send_presence/3, #{}, 75}, |
398 |
|
{user_receive_presence, HostType, fun ?MODULE:user_receive_presence/3, #{}, 1}, |
399 |
|
{c2s_stream_features, HostType, fun ?MODULE:caps_stream_features/3, #{}, 75}, |
400 |
|
{s2s_stream_features, HostType, fun ?MODULE:caps_stream_features/3, #{}, 75}, |
401 |
|
{disco_local_identity, HostType, fun ?MODULE:disco_local_identity/3, #{}, 1}, |
402 |
|
{disco_info, HostType, fun ?MODULE:disco_info/3, #{}, 1} |
403 |
|
]. |
404 |
|
|
405 |
|
-spec code_change(any(), state(), any()) -> {ok, state()}. |
406 |
:-( |
code_change(_OldVsn, State, _Extra) -> {ok, State}. |
407 |
|
|
408 |
|
-spec feature_request(mongoose_acc:t(), jid:lserver(), jid:jid(), caps(), [binary()]) -> |
409 |
|
mongoose_acc:t(). |
410 |
|
feature_request(Acc, LServer, From, Caps, [SubNode | Tail] = SubNodes) -> |
411 |
:-( |
Node = Caps#caps.node, |
412 |
:-( |
NodePair = {Node, SubNode}, |
413 |
:-( |
HostType = mongoose_acc:host_type(Acc), |
414 |
:-( |
case cache_tab:lookup(caps_features, NodePair, caps_read_fun(HostType, NodePair)) of |
415 |
|
{ok, Fs} when is_list(Fs) -> |
416 |
:-( |
feature_request(Acc, LServer, From, Caps, Tail); |
417 |
|
Other -> |
418 |
:-( |
NeedRequest = case Other of |
419 |
:-( |
{ok, TS} -> os:system_time(second) >= TS + (?BAD_HASH_LIFETIME); |
420 |
:-( |
_ -> true |
421 |
|
end, |
422 |
:-( |
F = fun (_From, _To, _Acc1, timeout) -> |
423 |
|
%% IQ request timed out, skip this node |
424 |
:-( |
feature_request(Acc, LServer, From, Caps, Tail); |
425 |
|
(_From, _To, Acc1, IQReply) -> |
426 |
:-( |
feature_response(Acc1, IQReply, LServer, From, Caps, SubNodes) |
427 |
|
end, |
428 |
:-( |
case NeedRequest of |
429 |
|
true -> |
430 |
:-( |
IQ = #iq{type = get, xmlns = ?NS_DISCO_INFO, |
431 |
|
sub_el = |
432 |
|
[#xmlel{name = <<"query">>, |
433 |
|
attrs = |
434 |
|
[{<<"xmlns">>, ?NS_DISCO_INFO}, |
435 |
|
{<<"node">>, |
436 |
|
<<Node/binary, "#", |
437 |
|
SubNode/binary>>}], |
438 |
|
children = []}]}, |
439 |
:-( |
cache_tab:insert(caps_features, NodePair, os:system_time(second), |
440 |
|
caps_write_fun(HostType, NodePair, os:system_time(second))), |
441 |
:-( |
ejabberd_local:route_iq(jid:make_noprep(<<>>, LServer, <<>>), From, Acc, IQ, F), |
442 |
:-( |
Acc; |
443 |
:-( |
false -> feature_request(Acc, LServer, From, Caps, Tail) |
444 |
|
end |
445 |
|
end; |
446 |
|
feature_request(Acc, _LServer, From, Caps, []) -> |
447 |
|
%% feature_request is never executed with empty SubNodes list |
448 |
|
%% so if we end up here, it means the caps are known |
449 |
:-( |
HostType = mongoose_acc:host_type(Acc), |
450 |
:-( |
mongoose_hooks:caps_recognised(Acc, From, self(), get_features_list(HostType, Caps)). |
451 |
|
|
452 |
|
-spec feature_response(mongoose_acc:t(), jlib:iq(), jid:lserver(), jid:jid(), caps(), [binary()]) -> |
453 |
|
mongoose_acc:t(). |
454 |
|
feature_response(Acc, #iq{type = result, sub_el = [#xmlel{children = Els}]}, |
455 |
|
LServer, From, Caps, [SubNode | SubNodes]) -> |
456 |
:-( |
HostType = mongoose_acc:host_type(Acc), |
457 |
:-( |
NodePair = {Caps#caps.node, SubNode}, |
458 |
:-( |
case check_hash(Caps, Els) of |
459 |
|
true -> |
460 |
:-( |
Features = lists:flatmap(fun (#xmlel{name = <<"feature">>, |
461 |
|
attrs = FAttrs}) -> |
462 |
:-( |
[xml:get_attr_s(<<"var">>, FAttrs)]; |
463 |
:-( |
(_) -> [] |
464 |
|
end, |
465 |
|
Els), |
466 |
:-( |
cache_tab:insert(caps_features, NodePair, |
467 |
|
Features, |
468 |
|
caps_write_fun(HostType, NodePair, Features)); |
469 |
:-( |
false -> ok |
470 |
|
end, |
471 |
:-( |
feature_request(Acc, LServer, From, Caps, SubNodes); |
472 |
|
feature_response(Acc, _IQResult, LServer, From, Caps, [_SubNode | SubNodes]) -> |
473 |
:-( |
feature_request(Acc, LServer, From, Caps, SubNodes). |
474 |
|
|
475 |
|
-spec caps_read_fun(mongooseim:host_type(), node_pair()) -> |
476 |
|
fun(() -> {ok, maybe_pending_features()} | error). |
477 |
|
caps_read_fun(HostType, Node) -> |
478 |
:-( |
fun () -> |
479 |
:-( |
mod_caps_backend:read(HostType, Node) |
480 |
|
end. |
481 |
|
|
482 |
|
-spec caps_write_fun(mongooseim:host_type(), node_pair(), maybe_pending_features()) -> |
483 |
|
fun(() -> ok). |
484 |
|
caps_write_fun(HostType, Node, Features) -> |
485 |
:-( |
fun () -> |
486 |
:-( |
mod_caps_backend:write(HostType, Node, Features) |
487 |
|
end. |
488 |
|
|
489 |
|
-spec delete_caps(mongooseim:host_type(), node_pair()) -> ok. |
490 |
|
delete_caps(HostType, Node) -> |
491 |
:-( |
cache_tab:delete(caps_features, Node, caps_delete_fun(HostType, Node)). |
492 |
|
|
493 |
|
-spec caps_delete_fun(mongooseim:host_type(), node_pair()) -> fun(() -> ok). |
494 |
|
caps_delete_fun(HostType, Node) -> |
495 |
:-( |
fun () -> |
496 |
:-( |
mod_caps_backend:delete_node(HostType, Node) |
497 |
|
end. |
498 |
|
|
499 |
|
-spec make_my_disco_hash(mongooseim:host_type(), jid:lserver()) -> binary(). |
500 |
|
make_my_disco_hash(HostType, LServer) -> |
501 |
58 |
JID = jid:make(<<>>, LServer, <<>>), |
502 |
58 |
case mongoose_disco:get_local_features(HostType, JID, JID, <<>>, <<>>) of |
503 |
|
empty -> |
504 |
:-( |
<<>>; |
505 |
|
{result, FeaturesXML} -> |
506 |
58 |
IdentityXML = mongoose_disco:get_local_identity(HostType, JID, JID, <<>>, <<>>), |
507 |
58 |
InfoXML = mongoose_disco:get_info(HostType, mod_disco, <<>>, <<>>), |
508 |
58 |
make_disco_hash(IdentityXML ++ InfoXML ++ FeaturesXML, sha1) |
509 |
|
end. |
510 |
|
|
511 |
|
-spec make_disco_hash([exml:element()], HashAlgorithm :: atom()) -> binary(). |
512 |
|
make_disco_hash(DiscoEls, Algo) -> |
513 |
58 |
Concat = list_to_binary([concat_identities(DiscoEls), |
514 |
|
concat_features(DiscoEls), concat_info(DiscoEls)]), |
515 |
58 |
jlib:encode_base64(case Algo of |
516 |
:-( |
md5 -> erlang:md5(Concat); |
517 |
58 |
sha1 -> crypto:hash(sha, Concat); |
518 |
:-( |
sha224 -> crypto:hash(sha224, Concat); |
519 |
:-( |
sha256 -> crypto:hash(sha256, Concat); |
520 |
:-( |
sha384 -> crypto:hash(sha384, Concat); |
521 |
:-( |
sha512 -> crypto:hash(sha512, Concat) |
522 |
|
end). |
523 |
|
|
524 |
|
check_hash(Caps, Els) -> |
525 |
:-( |
case Caps#caps.hash of |
526 |
|
<<"md5">> -> |
527 |
:-( |
Caps#caps.version == make_disco_hash(Els, md5); |
528 |
|
<<"sha-1">> -> |
529 |
:-( |
Caps#caps.version == make_disco_hash(Els, sha1); |
530 |
|
<<"sha-224">> -> |
531 |
:-( |
Caps#caps.version == make_disco_hash(Els, sha224); |
532 |
|
<<"sha-256">> -> |
533 |
:-( |
Caps#caps.version == make_disco_hash(Els, sha256); |
534 |
|
<<"sha-384">> -> |
535 |
:-( |
Caps#caps.version == make_disco_hash(Els, sha384); |
536 |
|
<<"sha-512">> -> |
537 |
:-( |
Caps#caps.version == make_disco_hash(Els, sha512); |
538 |
:-( |
_ -> true |
539 |
|
end. |
540 |
|
|
541 |
|
concat_features(Els) -> |
542 |
58 |
lists:usort(lists:flatmap(fun (#xmlel{name = |
543 |
|
<<"feature">>, |
544 |
|
attrs = Attrs}) -> |
545 |
696 |
[[xml:get_attr_s(<<"var">>, Attrs), $<]]; |
546 |
116 |
(_) -> [] |
547 |
|
end, |
548 |
|
Els)). |
549 |
|
|
550 |
|
concat_identities(Els) -> |
551 |
58 |
lists:sort(lists:flatmap(fun (#xmlel{name = |
552 |
|
<<"identity">>, |
553 |
|
attrs = Attrs}) -> |
554 |
58 |
[[xml:get_attr_s(<<"category">>, Attrs), |
555 |
|
$/, xml:get_attr_s(<<"type">>, Attrs), |
556 |
|
$/, |
557 |
|
xml:get_attr_s(<<"xml:lang">>, Attrs), |
558 |
|
$/, xml:get_attr_s(<<"name">>, Attrs), |
559 |
|
$<]]; |
560 |
754 |
(_) -> [] |
561 |
|
end, |
562 |
|
Els)). |
563 |
|
|
564 |
|
concat_info(Els) -> |
565 |
58 |
lists:sort(lists:flatmap(fun(El) -> |
566 |
812 |
concat_xdata_fields(mongoose_data_forms:parse_form(El)) |
567 |
|
end, |
568 |
|
Els)). |
569 |
|
|
570 |
|
concat_xdata_fields(#{type := <<"result">>, kvs := KVs, ns := NS}) -> |
571 |
58 |
Res = maps:fold(fun(Var, Values, VarFields) -> |
572 |
62 |
NewField = [[V, $<] || V <- [Var | lists:sort(Values)]], |
573 |
62 |
[NewField | VarFields] |
574 |
|
end, [], KVs), |
575 |
58 |
[[NS, $<, lists:sort(Res)]]; |
576 |
|
concat_xdata_fields(_) -> |
577 |
754 |
[]. |
578 |
|
|
579 |
|
gb_trees_fold(F, Acc, Tree) -> |
580 |
:-( |
Iter = gb_trees:iterator(Tree), |
581 |
:-( |
gb_trees_fold_iter(F, Acc, Iter). |
582 |
|
|
583 |
|
gb_trees_fold_iter(F, Acc, Iter) -> |
584 |
:-( |
case gb_trees:next(Iter) of |
585 |
|
{Key, Val, NewIter} -> |
586 |
:-( |
NewAcc = F(Key, Val, Acc), |
587 |
:-( |
gb_trees_fold_iter(F, NewAcc, NewIter); |
588 |
:-( |
_ -> Acc |
589 |
|
end. |
590 |
|
|
591 |
|
is_valid_node(Node) -> |
592 |
189 |
case mongoose_bin:tokens(Node, <<"#">>) of |
593 |
|
[?MONGOOSE_URI|_] -> |
594 |
6 |
true; |
595 |
|
_ -> |
596 |
183 |
false |
597 |
|
end. |