1 |
|
%% @doc MongooseIM/Ejabberd module for (a subset of) XEP-0079 support. |
2 |
|
%% This work was sponsored by Grindr LLC |
3 |
|
%% @reference <a href="http://xmpp.org/extensions/xep-0079.html">XEP-0079</a> |
4 |
|
%% @author <mongooseim@erlang-solutions.com> |
5 |
|
%% @copyright 2014 Erlang Solutions, Ltd. |
6 |
|
-module(mod_amp). |
7 |
|
|
8 |
|
-behavior(gen_mod). |
9 |
|
-behaviour(mongoose_module_metrics). |
10 |
|
-xep([{xep, 79}, {version, "1.2"}, {comment, "partially implemented."}]). |
11 |
|
-export([start/2, stop/1, supported_features/0]). |
12 |
|
-export([run_initial_check/2, |
13 |
|
check_packet/2, |
14 |
|
disco_local_features/1, |
15 |
|
c2s_stream_features/3 |
16 |
|
]). |
17 |
|
|
18 |
|
-ignore_xref([c2s_stream_features/3, disco_local_features/1, run_initial_check/2]). |
19 |
|
|
20 |
|
-include("amp.hrl"). |
21 |
|
-include("mongoose.hrl"). |
22 |
|
-include("jlib.hrl"). |
23 |
|
-include("ejabberd_c2s.hrl"). |
24 |
|
|
25 |
|
-define(AMP_FEATURE, |
26 |
|
#xmlel{name = <<"amp">>, attrs = [{<<"xmlns">>, ?NS_AMP_FEATURE}]}). |
27 |
|
-define(AMP_RESOLVER, amp_resolver). |
28 |
|
-define(AMP_STRATEGY, amp_strategy). |
29 |
|
|
30 |
|
-spec start(mongooseim:host_type(), gen_mod:module_opts()) -> ok. |
31 |
|
start(HostType, _Opts) -> |
32 |
132 |
ejabberd_hooks:add(hooks(HostType)). |
33 |
|
|
34 |
|
-spec stop(mongooseim:host_type()) -> ok. |
35 |
|
stop(HostType) -> |
36 |
128 |
ejabberd_hooks:delete(hooks(HostType)). |
37 |
|
|
38 |
|
-spec supported_features() -> [atom()]. |
39 |
66 |
supported_features() -> [dynamic_domains]. |
40 |
|
|
41 |
|
hooks(HostType) -> |
42 |
260 |
[{c2s_stream_features, HostType, ?MODULE, c2s_stream_features, 50}, |
43 |
|
{disco_local_features, HostType, ?MODULE, disco_local_features, 99}, |
44 |
|
{c2s_preprocessing_hook, HostType, ?MODULE, run_initial_check, 10}, |
45 |
|
{amp_verify_support, HostType, ?AMP_RESOLVER, verify_support, 10}, |
46 |
|
{amp_check_condition, HostType, ?AMP_RESOLVER, check_condition, 10}, |
47 |
|
{amp_determine_strategy, HostType, ?AMP_STRATEGY, determine_strategy, 10}]. |
48 |
|
|
49 |
|
%% API |
50 |
|
|
51 |
|
-spec run_initial_check(mongoose_acc:t(), ejabberd_c2s:state()) -> mongoose_acc:t(). |
52 |
|
run_initial_check(#{result := drop} = Acc, _C2SState) -> |
53 |
:-( |
Acc; |
54 |
|
run_initial_check(Acc, _C2SState) -> |
55 |
12003 |
case mongoose_acc:stanza_name(Acc) of |
56 |
3353 |
<<"message">> -> run_initial_check(Acc); |
57 |
8650 |
_ -> Acc |
58 |
|
end. |
59 |
|
|
60 |
|
-spec check_packet(mongoose_acc:t(), amp_event()) -> mongoose_acc:t(). |
61 |
|
check_packet(Acc, Event) -> |
62 |
35613 |
case mongoose_acc:get(amp, rules, none, Acc) of |
63 |
35316 |
none -> Acc; |
64 |
297 |
Rules -> process_event(Acc, Rules, Event) |
65 |
|
end. |
66 |
|
|
67 |
|
-spec disco_local_features(mongoose_disco:feature_acc()) -> mongoose_disco:feature_acc(). |
68 |
|
disco_local_features(Acc = #{node := Node}) -> |
69 |
77 |
case amp_features(Node) of |
70 |
4 |
[] -> Acc; |
71 |
73 |
Features -> mongoose_disco:add_features(Features, Acc) |
72 |
|
end. |
73 |
|
|
74 |
|
-spec c2s_stream_features([exml:element()], mongooseim:host_type(), jid:lserver()) -> |
75 |
|
[exml:element()]. |
76 |
|
c2s_stream_features(Acc, _HostType, _Lserver) -> |
77 |
9293 |
lists:keystore(<<"amp">>, #xmlel.name, Acc, ?AMP_FEATURE). |
78 |
|
|
79 |
|
%% Internal |
80 |
|
|
81 |
|
-spec run_initial_check(mongoose_acc:t()) -> mongoose_acc:t(). |
82 |
|
run_initial_check(Acc) -> |
83 |
3353 |
Packet = mongoose_acc:element(Acc), |
84 |
3353 |
From = mongoose_acc:from_jid(Acc), |
85 |
3353 |
To = mongoose_acc:to_jid(Acc), |
86 |
3353 |
Result = case amp:extract_requested_rules(Packet) of |
87 |
3081 |
none -> nothing_to_do; |
88 |
271 |
{rules, Rules} -> validate_and_process_rules(Packet, From, Rules, Acc); |
89 |
1 |
{errors, Errors} -> send_errors_and_drop(Packet, From, Errors, Acc) |
90 |
|
end, |
91 |
3353 |
case Result of |
92 |
|
nothing_to_do -> |
93 |
3081 |
Acc; |
94 |
|
drop -> |
95 |
63 |
mongoose_acc:set(hook, result, drop, Acc); |
96 |
|
NewRules -> |
97 |
209 |
Acc1 = mongoose_acc:set_permanent(amp, rules, NewRules, Acc), |
98 |
209 |
mongoose_acc:update_stanza(#{element => amp:strip_amp_el(Packet), |
99 |
|
from_jid => From, |
100 |
|
to_jid => To}, Acc1) |
101 |
|
end. |
102 |
|
|
103 |
|
-spec validate_and_process_rules(exml:element(), jid:jid(), amp_rules(), mongoose_acc:t()) -> |
104 |
|
amp_rules() | drop. |
105 |
|
validate_and_process_rules(Packet, From, Rules, Acc) -> |
106 |
271 |
VerifiedRules = verify_support(mongoose_acc:host_type(Acc), Rules), |
107 |
271 |
{Good, Bad} = lists:partition(fun is_supported_rule/1, VerifiedRules), |
108 |
271 |
ValidRules = [ Rule || {supported, Rule} <- Good ], |
109 |
271 |
case Bad of |
110 |
|
[{error, ValidationError, InvalidRule} | _] -> |
111 |
2 |
send_error_and_drop(Packet, From, ValidationError, InvalidRule, Acc); |
112 |
|
[] -> |
113 |
269 |
process_rules(Packet, From, initial_check, ValidRules, Acc) |
114 |
|
end. |
115 |
|
|
116 |
|
-spec process_event(mongoose_acc:t(), amp_rules(), amp_event()) -> mongoose_acc:t(). |
117 |
|
process_event(Acc, Rules, Event) when Event =/= initial_check -> |
118 |
297 |
Packet = mongoose_acc:element(Acc), |
119 |
297 |
From = mongoose_acc:from_jid(Acc), |
120 |
297 |
NewRules = process_rules(Packet, From, Event, Rules, Acc), |
121 |
297 |
mongoose_acc:set_permanent(amp, rules, NewRules, Acc). |
122 |
|
|
123 |
|
-spec amp_features(binary()) -> [mongoose_disco:feature()]. |
124 |
|
amp_features(?NS_AMP) -> |
125 |
1 |
[<<?NS_AMP/binary, Suffix/binary>> || Suffix <- amp_feature_suffixes()]; |
126 |
|
amp_features(<<>>) -> |
127 |
72 |
[?NS_AMP]; |
128 |
|
amp_features(_) -> |
129 |
4 |
[]. |
130 |
|
|
131 |
|
%% @doc This may eventually be configurable, but for now we return a constant list. |
132 |
|
amp_feature_suffixes() -> |
133 |
1 |
[<<>>, |
134 |
|
<<"?action=drop">>, |
135 |
|
<<"?action=notify">>, |
136 |
|
<<"?action=error">>, |
137 |
|
<<"?condition=deliver">>, |
138 |
|
<<"?condition=match-resource">>]. |
139 |
|
|
140 |
|
-spec process_rules(exml:element(), jid:jid(), amp_event(), amp_rules(), mongoose_acc:t()) -> |
141 |
|
amp_rules() | drop. |
142 |
|
process_rules(Packet, From, Event, Rules, Acc) -> |
143 |
566 |
HostType = mongoose_acc:host_type(Acc), |
144 |
566 |
To = mongoose_acc:to_jid(Acc), |
145 |
566 |
Strategy = determine_strategy(HostType, Packet, From, To, Event), |
146 |
566 |
RulesWithResults = apply_rules(fun(Rule) -> |
147 |
544 |
resolve_condition(HostType, Strategy, Event, Rule) |
148 |
|
end, Rules), |
149 |
566 |
PacketResult = take_action(Packet, From, RulesWithResults, Acc), |
150 |
566 |
return_result(PacketResult, Event, RulesWithResults). |
151 |
|
|
152 |
|
%% @doc hooks helpers |
153 |
|
-spec verify_support(mongooseim:host_type(), amp_rules()) -> [amp_rule_support()]. |
154 |
|
verify_support(HostType, Rules) -> |
155 |
271 |
mongoose_hooks:amp_verify_support(HostType, Rules). |
156 |
|
|
157 |
|
-spec determine_strategy(mongooseim:host_type(), exml:element(), jid:jid(), jid:jid(), |
158 |
|
amp_event()) -> |
159 |
|
amp_strategy(). |
160 |
|
determine_strategy(HostType, Packet, From, To, Event) -> |
161 |
566 |
mongoose_hooks:amp_determine_strategy(HostType, From, To, Packet, Event). |
162 |
|
|
163 |
|
apply_rules(F, Rules) -> |
164 |
566 |
[Rule#amp_rule{result = F(Rule)} || Rule <- Rules]. |
165 |
|
|
166 |
|
-spec resolve_condition(mongooseim:host_type(), amp_strategy(), amp_event(), amp_rule()) -> |
167 |
|
amp_match_result(). |
168 |
|
resolve_condition(HostType, Strategy, Event, Rule) -> |
169 |
544 |
Result = mongoose_hooks:amp_check_condition(HostType, Strategy, Rule), |
170 |
544 |
match_undecided_for_final_event(Rule, Event, Result). |
171 |
|
|
172 |
|
match_undecided_for_final_event(#amp_rule{condition = deliver}, Event, undecided) |
173 |
|
when Event =:= delivered; |
174 |
16 |
Event =:= delivery_failed -> match; |
175 |
528 |
match_undecided_for_final_event(_, _, Result) -> Result. |
176 |
|
|
177 |
|
-spec take_action(exml:element(), jid:jid(), amp_rules(), mongoose_acc:t()) -> pass | drop. |
178 |
|
take_action(Packet, From, Rules, Acc) -> |
179 |
566 |
case find(fun(#amp_rule{result = Result}) -> Result =:= match end, Rules) of |
180 |
429 |
not_found -> pass; |
181 |
137 |
{found, Rule} -> take_action_for_matched_rule(Packet, From, Rule, Acc) |
182 |
|
end. |
183 |
|
|
184 |
60 |
return_result(drop, initial_check, _Rules) -> drop; |
185 |
|
return_result(pass, _Event, Rules) -> |
186 |
506 |
lists:filter(fun(#amp_rule{result = Result}) -> |
187 |
424 |
Result =:= undecided |
188 |
|
end, Rules). |
189 |
|
|
190 |
|
-spec take_action_for_matched_rule(exml:element(), jid:jid(), amp_rule(), mongoose_acc:t()) -> |
191 |
|
pass | drop. |
192 |
|
take_action_for_matched_rule(Packet, From, #amp_rule{action = notify} = Rule, _Acc) -> |
193 |
77 |
reply_to_sender(Rule, server_jid(From), From, Packet), |
194 |
77 |
pass; |
195 |
|
take_action_for_matched_rule(Packet, From, #amp_rule{action = error} = Rule, Acc) -> |
196 |
30 |
send_error_and_drop(Packet, From, 'undefined-condition', Rule, Acc); |
197 |
|
take_action_for_matched_rule(Packet, From, #amp_rule{action = drop}, Acc) -> |
198 |
30 |
update_metric_and_drop(Packet, From, Acc). |
199 |
|
|
200 |
|
-spec reply_to_sender(amp_rule(), jid:jid(), jid:jid(), exml:element()) -> mongoose_acc:t(). |
201 |
|
reply_to_sender(MatchedRule, ServerJid, OriginalSender, OriginalPacket) -> |
202 |
77 |
Response = amp:make_response(MatchedRule, OriginalSender, OriginalPacket), |
203 |
77 |
ejabberd_router:route(ServerJid, OriginalSender, Response). |
204 |
|
|
205 |
|
-spec send_error_and_drop(exml:element(), jid:jid(), amp_error(), amp_rule(), mongoose_acc:t()) -> drop. |
206 |
|
send_error_and_drop(Packet, From, AmpError, MatchedRule, Acc) -> |
207 |
32 |
send_errors_and_drop(Packet, From, [{AmpError, MatchedRule}], Acc). |
208 |
|
|
209 |
|
-spec send_errors_and_drop(exml:element(), jid:jid(), [{amp_error(), amp_rule()}], mongoose_acc:t()) -> drop. |
210 |
|
send_errors_and_drop(Packet, From, [], Acc) -> |
211 |
:-( |
?LOG_ERROR(#{what => empty_list_of_errors_generated, |
212 |
|
text => <<"This shouldn't happen!">>, |
213 |
:-( |
exml_packet => Packet, from => From}), |
214 |
:-( |
update_metric_and_drop(Packet, From, Acc); |
215 |
|
send_errors_and_drop(Packet, From, ErrorRules, Acc) -> |
216 |
33 |
{Errors, Rules} = lists:unzip(ErrorRules), |
217 |
33 |
ErrorResponse = amp:make_error_response(Errors, Rules, From, Packet), |
218 |
33 |
ejabberd_router:route(server_jid(From), From, ErrorResponse), |
219 |
33 |
update_metric_and_drop(Packet, From, Acc). |
220 |
|
|
221 |
|
-spec update_metric_and_drop(exml:element(), jid:jid(), mongoose_acc:t()) -> drop. |
222 |
|
update_metric_and_drop(Packet, From, Acc) -> |
223 |
63 |
mongoose_hooks:xmpp_stanza_dropped(Acc, From, mongoose_acc:to_jid(Acc), Packet), |
224 |
63 |
drop. |
225 |
|
|
226 |
|
-spec is_supported_rule(amp_rule_support()) -> boolean(). |
227 |
403 |
is_supported_rule({supported, _}) -> true; |
228 |
2 |
is_supported_rule(_) -> false. |
229 |
|
|
230 |
|
server_jid(#jid{lserver = Host}) -> |
231 |
110 |
jid:from_binary(Host). |
232 |
|
|
233 |
429 |
find(_Pred, []) -> not_found; |
234 |
|
find(Pred, [H|T]) -> |
235 |
504 |
case Pred(H) of |
236 |
137 |
true -> {found, H}; |
237 |
367 |
false -> find(Pred, T) |
238 |
|
end. |