./ct_report/coverage/mod_carboncopy.COVER.html

1 %%%----------------------------------------------------------------------
2 %%% File : mod_carboncopy.erl
3 %%% Author : Eric Cestari <ecestari@process-one.net>
4 %%% Purpose : Message Carbons XEP-0280 0.8
5 %%% Created : 5 May 2008 by Mickael Remond <mremond@process-one.net>
6 %%% Usage : Add `mod_carboncopy` to the `modules` section of mongooseim.toml
7 %%%
8 %%%
9 %%% ejabberd, Copyright (C) 2002-2014 ProcessOne
10 %%%
11 %%% This program is free software; you can redistribute it and/or
12 %%% modify it under the terms of the GNU General Public License as
13 %%% published by the Free Software Foundation; either version 2 of the
14 %%% License, or (at your option) any later version.
15 %%%
16 %%% This program is distributed in the hope that it will be useful,
17 %%% but WITHOUT ANY WARRANTY; without even the implied warranty of
18 %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
19 %%% General Public License for more details.
20 %%%
21 %%% You should have received a copy of the GNU General Public License along
22 %%% with this program; if not, write to the Free Software Foundation, Inc.,
23 %%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
24 %%%
25 %%%----------------------------------------------------------------------
26 -module (mod_carboncopy).
27 -author ('ecestari@process-one.net').
28 -xep([{xep, 280}, {version, "1.0.1"}, {legacy_versions, ["0.6"]}]).
29 -behaviour(gen_mod).
30 -behaviour(mongoose_module_metrics).
31
32 %% API
33 -export([start/2,
34 stop/1,
35 hooks/1,
36 supported_features/0,
37 config_spec/0,
38 is_carbon_copy/1]).
39
40 %% Hooks
41 -export([disco_local_features/3,
42 bind2_stream_features/3,
43 bind2_enable_features/3,
44 user_send_message/3,
45 user_receive_message/3,
46 iq_handler2/5,
47 iq_handler1/5,
48 remove_connection/3
49 ]).
50
51 %% Tests
52 -export([should_forward/3]).
53
54 -ignore_xref([is_carbon_copy/1, should_forward/3]).
55
56 -define(CC_KEY, 'cc').
57
58 -include("mongoose.hrl").
59 -include("jlib.hrl").
60 -include("session.hrl").
61 -include("mongoose_config_spec.hrl").
62
63 -type direction() :: sent | received.
64
65 204 supported_features() -> [dynamic_domains].
66
67 is_carbon_copy(Packet) ->
68
:-(
case xml:get_subtag(Packet, <<"sent">>) of
69 #xmlel{name = <<"sent">>, attrs = AAttrs} ->
70
:-(
case xml:get_attr_s(<<"xmlns">>, AAttrs) of
71
:-(
?NS_CC_2 -> true;
72
:-(
?NS_CC_1 -> true;
73
:-(
_ -> false
74 end;
75
:-(
_ -> false
76 end.
77
78 %% Default IQDisc is no_queue:
79 %% executes disable/enable actions in the c2s process itself
80 start(HostType, #{iqdisc := IQDisc}) ->
81 411 gen_iq_handler:add_iq_handler_for_domain(HostType, ?NS_CC_2, ejabberd_sm,
82 fun ?MODULE:iq_handler2/5, #{}, IQDisc),
83 411 gen_iq_handler:add_iq_handler_for_domain(HostType, ?NS_CC_1, ejabberd_sm,
84 fun ?MODULE:iq_handler1/5, #{}, IQDisc).
85
86 stop(HostType) ->
87 411 gen_iq_handler:remove_iq_handler_for_domain(HostType, ?NS_CC_1, ejabberd_sm),
88 411 gen_iq_handler:remove_iq_handler_for_domain(HostType, ?NS_CC_2, ejabberd_sm),
89 411 ok.
90
91 hooks(HostType) ->
92 822 [
93 {disco_local_features, HostType, fun ?MODULE:disco_local_features/3, #{}, 99},
94 {bind2_stream_features, HostType, fun ?MODULE:bind2_stream_features/3, #{}, 50},
95 {bind2_enable_features, HostType, fun ?MODULE:bind2_enable_features/3, #{}, 50},
96 {unset_presence_hook, HostType, fun ?MODULE:remove_connection/3, #{}, 10},
97 {user_send_message, HostType, fun ?MODULE:user_send_message/3, #{}, 89},
98 {user_receive_message, HostType, fun ?MODULE:user_receive_message/3, #{}, 89}
99 ].
100
101 -spec config_spec() -> mongoose_config_spec:config_section().
102 config_spec() ->
103 208 #section{items = #{<<"iqdisc">> => mongoose_config_spec:iqdisc()},
104 defaults = #{<<"iqdisc">> => no_queue}}.
105
106 -spec disco_local_features(Acc, Params, Extra) -> {ok, Acc} when
107 Acc :: mongoose_disco:feature_acc(),
108 Params :: map(),
109 Extra :: gen_hook:extra().
110 disco_local_features(Acc = #{node := <<>>}, _, _) ->
111 229 NewAcc = mongoose_disco:add_features([?NS_CC_1, ?NS_CC_2, ?NS_CC_RULES], Acc),
112 229 {ok, NewAcc};
113 disco_local_features(Acc, _, _) ->
114 5 {ok, Acc}.
115
116 -spec bind2_stream_features(Acc, #{c2s_data := mongoose_c2s:data()}, gen_hook:extra()) ->
117 {ok, Acc} when Acc :: [exml:element()].
118 bind2_stream_features(Acc, _, _) ->
119 29 SmFeature = #xmlel{name = <<"feature">>, attrs = [{<<"var">>, ?NS_CC_2}]},
120 29 {ok, [SmFeature | Acc]}.
121
122 -spec bind2_enable_features(SaslAcc, mod_sasl2:c2s_state_data(), gen_hook:extra()) ->
123 {ok, SaslAcc} when SaslAcc :: mongoose_acc:t().
124 bind2_enable_features(SaslAcc, _, _) ->
125 10 #{request := BindRequest} = mod_bind2:get_bind_request(SaslAcc),
126 10 case exml_query:subelement_with_name_and_ns(BindRequest, <<"enable">>, ?NS_CC_2) of
127 undefined ->
128 9 {ok, SaslAcc};
129 _ ->
130 %% We modify the info here, which is what will be written by set_session later,
131 %% so that enabling carbons and opening a session are an atomic operation
132 1 SaslStateData = mod_sasl2:get_state_data(SaslAcc),
133 1 #{c2s_data := C2SData} = SaslStateData,
134 1 Info = mongoose_c2s:get_info(C2SData),
135 1 Info1 = maps:put(?CC_KEY, cc_ver_to_int(?NS_CC_2), Info),
136 1 C2SData1 = mongoose_c2s:set_info(C2SData, Info1),
137 1 SaslStateData1 = SaslStateData#{c2s_data => C2SData1},
138 1 SaslAcc2 = mod_sasl2:set_state_data(SaslAcc, SaslStateData1),
139 1 {ok, SaslAcc2}
140 end.
141
142 iq_handler2(Acc, From, _To, IQ, _Extra) ->
143 110 iq_handler(Acc, From, IQ, ?NS_CC_2).
144 iq_handler1(Acc, From, _To, IQ, _Extra) ->
145
:-(
iq_handler(Acc, From, IQ, ?NS_CC_1).
146
147 iq_handler(Acc, From, #iq{type = set,
148 sub_el = #xmlel{name = Operation,
149 children = []}} = IQ, CC) ->
150 110 ?LOG_DEBUG(#{what => cc_iq_received, acc => Acc}),
151 110 case Operation of
152 <<"enable">> ->
153 109 enable(From, CC, Acc);
154 <<"disable">> ->
155 1 disable(From, Acc)
156 end,
157 110 {Acc, IQ#iq{type = result, sub_el = []}};
158 iq_handler(Acc, _From, IQ, _CC) ->
159
:-(
{Acc, IQ#iq{type = error, sub_el = [mongoose_xmpp_errors:bad_request()]}}.
160
161 -spec user_send_message(mongoose_acc:t(), mongoose_c2s_hooks:params(), gen_hook:extra()) ->
162 mongoose_c2s_hooks:result().
163 user_send_message(Acc, _, _) ->
164 4202 {From, To, Packet} = mongoose_acc:packet(Acc),
165 4202 check_and_forward(Acc, From, To, Packet, sent),
166 4202 {ok, Acc}.
167
168 -spec user_receive_message(mongoose_acc:t(), mongoose_c2s_hooks:params(), gen_hook:extra()) ->
169 mongoose_c2s_hooks:result().
170 user_receive_message(Acc, #{c2s_data := C2SData}, _) ->
171 12492 JID = mongoose_c2s:get_jid(C2SData),
172 12492 {_, To, Packet} = mongoose_acc:packet(Acc),
173 12492 check_and_forward(Acc, JID, To, Packet, received),
174 12492 {ok, Acc}.
175
176 -spec remove_connection(Acc, Params, Extra) -> {ok, Acc} when
177 Acc :: mongoose_acc:t(),
178 Params :: #{jid := jid:jid()},
179 Extra :: gen_hook:extra().
180 remove_connection(Acc, #{jid := JID}, _) ->
181 5803 disable(JID, Acc),
182 5803 {ok, Acc}.
183
184 % Check if the traffic is local.
185 % Modified from original version:
186 % - registered to the user_send_message hook, to be called only once even for multicast
187 % - do not support "private" message mode, and do not modify the original packet in any way
188 % - we also replicate "read" notifications
189 -spec check_and_forward(mongoose_acc:t(), jid:jid(), jid:jid(), exml:element(), direction()) -> ok | stop.
190 check_and_forward(Acc, JID, To, Packet, Direction) ->
191 16694 case should_forward(Packet, To, Direction) of
192 10069 false -> stop;
193 6625 true -> send_copies(Acc, JID, To, Packet, Direction)
194 end.
195
196 %%%===================================================================
197 %%% Classification
198 %%%===================================================================
199
200 -spec should_forward(exml:element(), jid:jid(), direction()) -> boolean().
201 should_forward(Packet, To, Direction) ->
202 16694 (not is_carbon_private(Packet)) andalso
203 16692 (not has_nocopy_hint(Packet)) andalso
204 16692 (not is_received(Packet)) andalso
205 16658 (not is_sent(Packet)) andalso
206 16637 (is_chat(Packet) orelse is_valid_muc(Packet, To, Direction)).
207
208 -spec is_chat(exml:element()) -> boolean().
209 is_chat(Packet) ->
210 16637 case exml_query:attr(Packet, <<"type">>, <<"normal">>) of
211 5961 <<"normal">> -> contains_body(Packet) orelse
212 5953 contains_receipts(Packet) orelse
213 5951 contains_csn(Packet) orelse
214 5949 contains_chat_markers(Packet);
215 6337 <<"chat">> -> true;
216 4339 _ -> false
217 end.
218
219 -spec is_valid_muc(exml:element(), jid:jid(), direction()) -> boolean().
220 is_valid_muc(_, _, sent) ->
221 841 false;
222 is_valid_muc(Packet, To, _) ->
223 9229 is_mediated_invitation(Packet) orelse
224 9218 is_direct_muc_invitation(Packet) orelse
225 9206 is_received_private_muc(Packet, To).
226
227 -spec is_mediated_invitation(exml:element()) -> boolean().
228 is_mediated_invitation(Packet) ->
229 9229 undefined =/= exml_query:path(Packet,
230 [{element_with_ns, <<"x">>, ?NS_MUC_USER},
231 {element, <<"invite">>},
232 {attr, <<"from">>}]).
233
234 -spec is_direct_muc_invitation(exml:element()) -> boolean().
235 is_direct_muc_invitation(Packet) ->
236 9218 undefined =/= exml_query:subelement_with_name_and_ns(Packet, <<"x">>, ?NS_CONFERENCE).
237
238 -spec is_received_private_muc(exml:element(), jid:jid()) -> boolean().
239 is_received_private_muc(_, #jid{lresource = <<>>}) ->
240 1853 false;
241 is_received_private_muc(Packet, _) ->
242 7353 undefined =/= exml_query:subelement_with_name_and_ns(Packet, <<"x">>, ?NS_MUC_USER).
243
244 -spec has_nocopy_hint(exml:element()) -> boolean().
245 has_nocopy_hint(Packet) ->
246 16692 undefined =/= exml_query:subelement_with_name_and_ns(Packet, <<"no-copy">>, ?NS_HINTS).
247
248 -spec contains_body(exml:element()) -> boolean().
249 contains_body(Packet) ->
250 5961 undefined =/= exml_query:subelement(Packet, <<"body">>).
251
252 -spec contains_receipts(exml:element()) -> boolean().
253 contains_receipts(Packet) ->
254 5953 undefined =/= exml_query:subelement_with_name_and_ns(Packet, <<"received">>, ?NS_RECEIPTS).
255
256 -spec contains_csn(exml:element()) -> boolean().
257 contains_csn(Packet) ->
258 5951 undefined =/= exml_query:subelement_with_ns(Packet, ?NS_CHATSTATES).
259
260 -spec contains_chat_markers(exml:element()) -> boolean().
261 contains_chat_markers(Packet) ->
262 5949 undefined =/= exml_query:subelement_with_ns(Packet, ?NS_CHAT_MARKERS).
263
264 -spec is_carbon_private(exml:element()) -> boolean().
265 is_carbon_private(Packet) ->
266 16694 [] =/= subelements_with_nss(Packet, <<"private">>, carbon_namespaces()).
267
268 -spec is_received(exml:element()) -> boolean().
269 is_received(Packet) ->
270 16692 [] =/= subelements_with_nss(Packet, <<"received">>, carbon_namespaces()).
271
272 -spec is_sent(exml:element()) -> boolean().
273 is_sent(Packet) ->
274 16658 [] =/= subelements_with_nss(Packet, <<"sent">>, carbon_namespaces()).
275
276 -spec subelements_with_nss(exml:element(), binary(), [binary()]) -> [exml:element()].
277 subelements_with_nss(#xmlel{children = Children}, Name, NSS) ->
278 50044 lists:filter(fun(#xmlel{name = N} = Child) when N =:= Name ->
279 189 NS = exml_query:attr(Child, <<"xmlns">>),
280 189 lists:member(NS, NSS);
281 (_) ->
282 62997 false
283 end, Children).
284
285 50044 carbon_namespaces() -> [?NS_CC_1, ?NS_CC_2].
286
287 %%%===================================================================
288 %%% Internal
289 %%%===================================================================
290
291
292 %%
293 %% Internal
294 %%
295 is_bare_to(Direction, To, _PrioRes) ->
296 6625 case {Direction, To} of
297 368 {received, #jid{lresource = <<>>}} -> true;
298 6257 _ -> false
299 end.
300
301 max_prio(PrioRes) ->
302 75 case catch lists:max(PrioRes) of
303 75 {Prio, _Res} -> Prio;
304
:-(
_ -> 0
305 end.
306
307 is_max_prio(Res, PrioRes) ->
308 75 lists:member({max_prio(PrioRes), Res}, PrioRes).
309
310 jids_minus_max_priority_resource(JID, CCResList, PrioRes) ->
311 368 [ {jid:replace_resource(JID, CCRes), CCVersion}
312 368 || {CCVersion, CCRes} <- CCResList, not is_max_prio(CCRes, PrioRes) ].
313
314 jids_minus_specific_resource(JID, R, CCResList, _PrioRes) ->
315 6257 [ {jid:replace_resource(JID, CCRes), CCVersion}
316 6257 || {CCVersion, CCRes} <- CCResList, CCRes =/= R ].
317
318 %% Direction = received | sent <received xmlns='urn:xmpp:carbons:1'/>
319 send_copies(Acc, JID, To, Packet, Direction) ->
320 6625 #jid{lresource = R} = JID,
321 6625 {PrioRes, CCResList} = get_cc_enabled_resources(JID),
322 6625 Targets = case is_bare_to(Direction, To, PrioRes) of
323 368 true -> jids_minus_max_priority_resource
324 (JID, CCResList, PrioRes);
325 6257 _ -> jids_minus_specific_resource(JID, R, CCResList, PrioRes)
326 end,
327 6625 ?LOG_DEBUG(#{what => cc_send_copies,
328 6625 targets => Targets, resources => PrioRes, ccenabled => CCResList}),
329 6625 lists:foreach(fun({Dest, Version}) ->
330 53 ?LOG_DEBUG(#{what => cc_forwarding,
331 user => JID#jid.luser, server => JID#jid.lserver,
332 53 resource => JID#jid.lresource, exml_packet => Packet}),
333 53 Sender = jid:to_bare(JID),
334 53 New = build_forward_packet(Acc, JID, Packet, Sender, Dest, Direction, Version),
335 53 ejabberd_router:route(Sender, Dest, Acc, New)
336 end, Targets).
337
338 build_forward_packet(Acc, JID, Packet, Sender, Dest, Direction, Version) ->
339 % The wrapping message SHOULD maintain the same 'type' attribute value;
340 53 Type = exml_query:attr(Packet, <<"type">>, <<"normal">>),
341 53 #xmlel{name = <<"message">>,
342 attrs = [{<<"xmlns">>, <<"jabber:client">>},
343 {<<"type">>, Type},
344 {<<"from">>, jid:to_binary(Sender)},
345 {<<"to">>, jid:to_binary(Dest)}],
346 children = carbon_copy_children(Acc, Version, JID, Packet, Direction)}.
347
348 carbon_copy_children(Acc, ?NS_CC_1, JID, Packet, Direction) ->
349
:-(
[ #xmlel{name = atom_to_binary(Direction, utf8),
350 attrs = [{<<"xmlns">>, ?NS_CC_1}]},
351 #xmlel{name = <<"forwarded">>,
352 attrs = [{<<"xmlns">>, ?NS_FORWARD}],
353 children = [complete_packet(Acc, JID, Packet, Direction)]} ];
354 carbon_copy_children(Acc, ?NS_CC_2, JID, Packet, Direction) ->
355 53 [ #xmlel{name = atom_to_binary(Direction, utf8),
356 attrs = [{<<"xmlns">>, ?NS_CC_2}],
357 children = [ #xmlel{name = <<"forwarded">>,
358 attrs = [{<<"xmlns">>, ?NS_FORWARD}],
359 children = [complete_packet(Acc, JID, Packet, Direction)]} ]} ].
360
361 enable(JID, CC, Acc) ->
362 109 ?LOG_INFO(#{what => cc_enable,
363 109 user => JID#jid.luser, server => JID#jid.lserver}),
364 109 OriginSid = mongoose_acc:get(c2s, origin_sid, Acc),
365 109 ejabberd_sm:store_info(JID, OriginSid, ?CC_KEY, cc_ver_to_int(CC)).
366
367 disable(JID, Acc) ->
368 5804 ?LOG_INFO(#{what => cc_disable,
369 5804 user => JID#jid.luser, server => JID#jid.lserver}),
370 5804 OriginSid = mongoose_acc:get(c2s, origin_sid, Acc),
371 5804 ejabberd_sm:remove_info(JID, OriginSid, ?CC_KEY).
372
373 complete_packet(Acc, From, #xmlel{name = <<"message">>, attrs = OrigAttrs} = Packet, sent) ->
374 %% if this is a packet sent by user on this host, then Packet doesn't
375 %% include the 'from' attribute. We must add it.
376 21 Attrs = lists:keystore(<<"xmlns">>, 1, OrigAttrs, {<<"xmlns">>, <<"jabber:client">>}),
377 21 Packet2 = set_stanza_id(Acc, From, Packet),
378 21 case proplists:get_value(<<"from">>, Attrs) of
379 undefined ->
380 21 Packet2#xmlel{attrs = [{<<"from">>, jid:to_binary(From)} | Attrs]};
381 _ ->
382
:-(
Packet2#xmlel{attrs = Attrs}
383 end;
384
385 complete_packet(_Acc, _From, #xmlel{name = <<"message">>, attrs = OrigAttrs} = Packet, received) ->
386 32 Attrs = lists:keystore(<<"xmlns">>, 1, OrigAttrs, {<<"xmlns">>, <<"jabber:client">>}),
387 32 Packet#xmlel{attrs = Attrs}.
388
389 get_cc_enabled_resources(JID) ->
390 6625 AllSessions = ejabberd_sm:get_raw_sessions(JID),
391 6625 CCs = filter_cc_enabled_resources(AllSessions),
392 6625 Prios = filter_priority_resources(AllSessions),
393 6625 {Prios, CCs}.
394
395 filter_cc_enabled_resources(AllSessions) ->
396 6625 lists:filtermap(fun fun_filter_cc_enabled_resource/1, AllSessions).
397
398 fun_filter_cc_enabled_resource(Session = #session{usr = {_, _, R}}) ->
399 7067 case mongoose_session:get_info(Session, ?CC_KEY, undefined) of
400 {?CC_KEY, V} when is_integer(V) ->
401 173 {true, {cc_ver_from_int(V), R}};
402 _ ->
403 6894 false
404 end.
405
406 filter_priority_resources(AllSessions) ->
407 6625 lists:filtermap(fun fun_filter_priority_resources/1, AllSessions).
408
409 fun_filter_priority_resources(#session{usr = {_, _, R}, priority = P})
410 when is_integer(P) ->
411 7024 {true, {P, R}};
412 fun_filter_priority_resources(_) ->
413 43 false.
414
415
:-(
cc_ver_to_int(?NS_CC_1) -> 1;
416 110 cc_ver_to_int(?NS_CC_2) -> 2.
417
418
:-(
cc_ver_from_int(1) -> ?NS_CC_1;
419 173 cc_ver_from_int(2) -> ?NS_CC_2.
420
421 %% Servers SHOULD include the element as a child
422 %% of the forwarded message when using Message Carbons (XEP-0280)
423 %% https://xmpp.org/extensions/xep-0313.html#archives_id
424 set_stanza_id(Acc, From, Packet) ->
425 21 MamId = mongoose_acc:get(mam, mam_id, undefined, Acc),
426 21 set_stanza_id(MamId, From, Acc, Packet).
427
428 set_stanza_id(undefined, _From, _Acc, Packet) ->
429 14 Packet;
430 set_stanza_id(MamId, From, _Acc, Packet) ->
431 7 By = jid:to_bare_binary(From),
432 7 mod_mam_utils:replace_arcid_elem(<<"stanza-id">>, By, MamId, Packet).
Line Hits Source